ADDED:
* math, time functions to tplx/sprigx
FIXED:
* logging not initializing properly on some BSDs
This commit is contained in:
brent saner
2026-01-30 06:35:23 -05:00
parent 1bd6e1256c
commit 07e0e587fa
16 changed files with 6831 additions and 260 deletions

View File

@@ -17,7 +17,7 @@ Last rendered {localdatetime}
[id="wat"]
== What is SprigX?
SprigX are extensions to https://masterminds.github.io/sprig/[the `sprig` library^] (https://pkg.go.dev/github.com/Masterminds/sprig/v3[Go docs^]).
SprigX is a suite of extensions to https://masterminds.github.io/sprig/[the `sprig` library^] (https://pkg.go.dev/github.com/Masterminds/sprig/v3[Go docs^]).
They provide functions that offer more enriched use cases and domain-specific data.
@@ -102,6 +102,8 @@ var txtTpl *template.Template = template.
----
====
Or, as a convenience, you can simply use the <<lib_cmbtfmap, `sprigx.CombinedTxtFuncMap`>> and/or <<lib_cmbhfmap, `sprigx.CombinedHtmlFuncMap`>> functions.
If a `<template>.FuncMap` is added via `.Funcs()` *after* template parsing, it will override any functions of the same name of a `<template>.FuncMap` *before* parsing.
For example, if both `sprig` and `sprigx` provide a function `foo`:
@@ -212,8 +214,103 @@ var (
----
====
[id="lib"]
== Library Functions
These are generally intended to be used *outside* the template in the actual Go code.
[id="lib_cmbfmap"]
=== `CombinedFuncMap`
[source,go]
.Function Signature
----
func CombinedFuncMap(preferSprigX bool) (fmap map[string]any)
----
This function returns a generic function map (like <<lib_fmap>>) combined with
https://pkg.go.dev/github.com/Masterminds/sprig/v3#GenericFuncMap[`github.com/Masterminds/sprig/v3.GenericFuncMap`^].
If `preferSprigx` is true, SprigX function names will override Sprig functions with the same name.
If false, Sprig functions will override conflicting SprigX functions with the same name.
You probably want <<lib_cmbtfmap>> or <<lib_cmbhfmap>> instead,
as they wrap this with the appropriate type.
[id="lib_cmbhfmap"]
=== `CombinedHtmlFuncMap`
[source,go]
.Function Signature
----
func CombinedHtmlFuncMap(preferSprigX bool) (fmap (html/template).FuncMap)
----
This function returns an https://pkg.go.dev/html/template#FuncMap[`(html/template).FuncMap`] function map (like <<lib_hfmap>>) combined with
https://pkg.go.dev/github.com/Masterminds/sprig/v3#HtmlFuncMap[`github.com/Masterminds/sprig/v3.HtmlFuncMap`^].
If `preferSprigx` is true, SprigX function names will override Sprig functions with the same name.
If false, Sprig functions will override conflicting SprigX functions with the same name.
[id="lib_cmbtfmap"]
=== `CombinedTxtFuncMap`
[source,go]
.Function Signature
----
func CombinedTxtFuncMap(preferSprigX bool) (fmap (text/template).FuncMap)
----
This function returns a https://pkg.go.dev/text/template#FuncMap[`(text/template).FuncMap`] function map (like <<lib_tfmap>>) combined with
https://pkg.go.dev/github.com/Masterminds/sprig/v3#TxtFuncMap[`github.com/Masterminds/sprig/v3.TxtFuncMap`^].
If `preferSprigx` is true, SprigX function names will override Sprig functions with the same name.
If false, Sprig functions will override conflicting SprigX functions with the same name.
[id="lib_fmap"]
=== `FuncMap`
[source,go]
.Function Signature
----
func FuncMap() (fmap map[string]any)
----
This function returns a generic SprigX function map.
You probably want <<lib_tfmap>> or <<lib_hfmap>> instead,
as they wrap this with the appropriate type.
[id="lib_hfmap"]
=== `HtmlFuncMap`
[source,go]
.Function Signature
----
func HtmlFuncMap() (fmap (html/template).FuncMap)
----
This function returns a SprigX https://pkg.go.dev/html/template#FuncMap[`(html/template).FuncMap`^].
[id="lib_nop"]
==== `Nop`
[source,go]
.Function Signature
----
func Nop(obj ...any) (s string)
----
`Nop` is a NO-OP function that one can use in an <<override, override map>> to explicitly disable
certain Sprig/SprigX functions that may be deemed "unsafe" and/or to sanitize templates from untrusted input.
It will *never* error or panic, and `s` is *always* an empty string.
[id="lib_tfmap"]
=== `TxtFuncMap`
[source,go]
.Function Signature
----
func TxtFuncMap() (fmap (text/template).FuncMap)
----
This function returns a SprigX https://pkg.go.dev/text/template#FuncMap[`(text/template).FuncMap`^].
[id="fn"]
== Functions
== Template Functions
Expect this list to grow over time, and potentially more frequently than the `sprigx` functions.
Each function includes its function signature to indicate what types of arguments/parameters it accepts,
@@ -248,19 +345,50 @@ func metaIsNil(obj any) (isNil bool)
This function fills in the gap that https://pkg.go.dev/text/template#IsTrue[`text/template.IsTrue`^] and https://pkg.go.dev/html/template#IsTrue[`html/template.IsTrue`^] (expressed in templates as `{{ if ... }}`) leaves, as those functions/expressions return false for e.g. `false` booleans AND nils.
[id="fn_meta_nop"]
==== `Nop`
[id="fn_num"]
=== Numbers/Math
[id="fn_num_f32s"]
==== `numFloat32Str`
[source,go]
.Function Signature
----
func Nop(obj ...any) (s string)
func numFloat32Str(f float32) (s string)
----
`Nop` is not actually a *template* function, but rather an exported *Go* function that one can use in
an <<override, override map>> to explicitly disable certain template functions that may be deemed "unsafe" and/or to sanitize templates from
untrusted input.
`numFloat32Str` returns a *complete* non-truncated non-right-padded string representation of a `float32`.
It will *never* error or panic, and `s` is *always* an empty string.
[id="fn_num_f64"]
==== `numFloat64`
[source,go]
.Function Signature
----
func numFloat64(val any) (f float64, err error)
----
`numFloat64` returns any string representation of a numeric value or any type of numeric value to a `float64`.
[id="fn_num_f64s"]
==== `numFloat64Str`
[source,go]
.Function Signature
----
func numFloat64Str(f float64) (s string)
----
`numFloat64Str` returns a *complete* non-truncated non-right-padded string representation of a `float64`.
[id="fn_num_fs"]
==== `numFloatStr`
[source,go]
.Function Signature
----
func numFloatStr(val any) (s string, err error)
----
`numFloatStr` wraps <<fn_num_f32s>> and <<fn_num_f64s>>.
`val` can be a string representation of any numeric value or any type of numeric value.
[id="fn_os"]
=== Operating System
@@ -818,7 +946,7 @@ renders as:
|===
[id="fn_ps"]
=== PSUtils
=== PSUtil
These are functions from https://pkg.go.dev/github.com/shirou/gopsutil/v4[`github.com/shirou/gopsutil/v4`^] packages.
[id="fn_ps_cpu"]
@@ -1386,6 +1514,173 @@ Where:
* `<indentString>`: The string to use as the "indent character". This can be any string, such as `" "`, `"\t"`, `"."`, `"|"`, `"=="` etc.
* `<input>`: The text to be indented. Because it is the last argument, `extIndent` works with pipelined text as well.
[id="fn_tm"]
=== Time/Dates/Timestamps
[NOTE]
====
Some of these functions duplicate Sprig functionality, but are included here for predictable API.
Care has been taken to name these functions differently from the Sprig functions where possible and sensible.
====
[id="fn_tm_date"]
==== `tmDate`
[source,go]
.Function Signature
----
func tmDate(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) (date time.Time)
----
`tmDate` directly calls https://pkg.go.dev/time#Date[`time.Date`^].
[id="fn_tm_fltmic"]
==== `tmFloatMicro`
[source,go]
.Function Signature
----
func tmFloatMicro(t time.Time) (f64 float64)
----
`tmFloatMicro` directly calls https://pkg.go.dev/r00t2.io/goutils/timex#F64Microseconds[`(r00t2.io/goutils/timex).F64Microseconds`^].
[id="fn_tm_fltmill"]
==== `tmFloatMilli`
[source,go]
.Function Signature
----
func tmFloatMilli(t time.Time) (f64 float64)
----
`tmFloatMilli` directly calls https://pkg.go.dev/r00t2.io/goutils/timex#F64Milliseconds[`(r00t2.io/goutils/timex).F64Milliseconds`^].
[id="fn_tm_fltnano"]
==== `tmFloatNano`
[source,go]
.Function Signature
----
func tmFloatNano(t time.Time) (f64 float64)
----
`tmFloatNano` directly calls https://pkg.go.dev/r00t2.io/goutils/timex#F64Nanoseconds[`(r00t2.io/goutils/timex).F64Nanoseconds`^].
[id="fn_tm_flt"]
==== `tmFloat`
[source,go]
.Function Signature
----
func tmFloat(t time.Time) (f64 float64)
----
`tmFloat` directly calls https://pkg.go.dev/r00t2.io/goutils/timex#F64Seconds[`(r00t2.io/goutils/timex).F64Seconds`^].
[id="fn_tm_fmt"]
==== `tmFmt`
[source,go]
.Function Signature
----
func tmFmt(fstr string, t time.Time) (out string)
----
`tmFormat` provides a more pipeline-friendly alternative to calling e.g.
[source,gotemplate]
----
{{- $t := tmNow -}}
{{ $t.Format "<some time format string>" }}
----
[id="fn_tm_now"]
==== `tmNow`
[source,go]
.Function Signature
----
func tmNow() (now time.Time)
----
`tmNow` directly calls https://pkg.go.dev/time#Now[`time.Now`^].
[id="fn_tm_pdur8n"]
==== `tmParseDur8n`
[source,go]
.Function Signature
----
func tmParseDur8n(s string) (d time.Duration, err error)
----
`tmParseDur8n` directly calls https://pkg.go.dev/time#ParseDuration[`time.ParseDuration`^].
[id="fn_tm_pmnth"]
==== `tmParseMonth`
[source,go]
.Function Signature
----
func tmParseMonth(v any) (mon time.Month, err error)
----
`tmParseMonth` attempts to first try <<fn_tm_pmnthi>> and then tries <<fn_tm_pmnths>> if `v` is not "numeric".
[id="fn_tm_pmnthi"]
==== `tmParseMonthInt`
[source,go]
.Function Signature
----
func tmParseMonthInt(n any) (mon time.Month, err error)
----
`tmParseMonthInt` parses a number representation of month `n` to a https://pkg.go.dev/time#Month[`time.Month`^].
`n` may be any numeric type or a string representation of a number (or a custom type derived from those).
A negative integer (or float, etc.) will be converted to a positive one (e.g. `-6` -> `6` -> `time.June`).
Floats are rounded to the nearest integer.
The integer should map directly to the https://pkg.go.dev/time#example-Month[`time.Month` constants^] in the `time` module:
* `1`: `time.January`
* `2`: `time.February`
* `3`: `time.March`
* `4`: `time.April`
* `5`: `time.May`
* `6`: `time.June`
* `7`: `time.July`
* `8`: `time.August`
* `9`: `time.September`
* `10`: `time.October`
* `11`: `time.November`
* `12`: `time.December`
If `n` resolves to `0`, `mon` will be the current month (as determined by https://pkg.go.dev/time#Now[`time.Now`^]).
If `n` resolves to > `12`, `err` will be `sprigx.ErrBadMonth` (though be sure to use https://pkg.go.dev/errors#Is[`errors.Is`^]; it will be wrapped by link:https://pkg.go.dev/text/template#ExecError[`(text/template).ExecError`]/link:https://pkg.go.dev/html/template#ExecError[`(html/template).ExecError`].
[id="fn_tm_pmnths"]
==== `tmParseMonthStr`
[source,go]
.Function Signature
----
func tmParseMonthStr(s string) (mon time.Month, err error)
----
`tmParseMonthStr` parses a string representation `s` of a month to a https://pkg.go.dev/time#Month[`time.Month`^].
It normalizes `s` to lowercase and only uses the first 3 characters (the minimum length needed to determine month name
uniqueness - "June" vs. "July", "March" vs. "May").
An empty (or whitespace-only) string will use the current month (as determined by https://pkg.go.dev/time#Now[`time.Now`^]).`
[id="fn_tm_ptm"]
==== `tmParseTime`
[source,go]
.Function Signature
----
func tmParseTime(layout, value string) (t time.Time, err error)
----
`tmParseTime` directly calls https://pkg.go.dev/time#Parse[`time.Parse`^].
Be sure that `layout` is a properly parseable https://pkg.go.dev/time#Layout[layout format^].
[id="fn_sys"]
=== System/Platform/Architecture

File diff suppressed because it is too large Load Diff

5303
tplx/sprigx/README.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ import (
`path`
`path/filepath`
`runtime`
`time`
`github.com/davecgh/go-spew/spew`
`github.com/shirou/gopsutil/v4/cpu`
@@ -16,6 +17,7 @@ import (
psnet `github.com/shirou/gopsutil/v4/net`
`github.com/shirou/gopsutil/v4/process`
`github.com/shirou/gopsutil/v4/sensors`
`r00t2.io/goutils/timex`
`r00t2.io/sysutils`
)
@@ -28,6 +30,13 @@ var (
"Meta"/Template-Helpers
*/
"metaIsNil": metaIsNil,
/*
Numbers/Math
*/
"numFloat32Str": numFloat32Str,
"numFloat64": numFloat64,
"numFloat64Str": numFloat64Str,
"numFloatStr": numFloatStr,
/*
OS
*/
@@ -119,7 +128,21 @@ var (
"sysNumCpu": runtime.NumCPU,
"sysOsName": sysOsNm,
"sysRuntime": sysRuntime,
/*
Time/Dates/Timestamps
*/
"tmDate": time.Date,
"tmFmt": tmFmt,
"tmFloatMicro": timex.F64Microseconds,
"tmFloatMilli": timex.F64Milliseconds,
"tmFloatNano": timex.F64Nanoseconds,
"tmFloat": timex.F64Seconds,
"tmNow": time.Now,
"tmParseDur8n": time.ParseDuration,
"tmParseMonth": tmParseMonth,
"tmParseMonthInt": tmParseMonthInt,
"tmParseMonthStr": tmParseMonthStr,
"tmParseTime": time.Parse,
}
// htmlMap holds functions usable/intended for use in only an [html/template.FuncMap].

11
tplx/sprigx/errs.go Normal file
View File

@@ -0,0 +1,11 @@
package sprigx
import (
`errors`
)
var (
ErrBadMonth error = errors.New("could not determine/parse month")
ErrBadType error = errors.New("an invalid/unknown type was passed")
ErrNilVal error = errors.New("a nil value was passed")
)

View File

@@ -1,14 +1,116 @@
package sprigx
import (
`errors`
htpl "html/template"
`math`
`reflect`
`strconv`
ttpl "text/template"
`github.com/Masterminds/sprig/v3`
)
/*
Many of these functions are modeled after sprig's.
*/
/*
CombinedFuncMap returns a generic function map (like [FuncMap]) combined with
[github.com/Masterminds/sprig/v3.GenericFuncMap].
If preferSprigx is true, SprigX function names will override Sprig
functions with the same name.
If false, Sprig functions will override conflicting SprigX functions
with the same name.
You probably want [CombinedHtmlFuncMap] or [CombinedTxtFuncMap] instead,
as they wrap this with the appropriate type.
*/
func CombinedFuncMap(preferSprigX bool) (fmap map[string]any) {
var fn any
var fnNm string
var sprigMap map[string]interface{} = sprig.GenericFuncMap()
var sprigxMap map[string]any = FuncMap()
if preferSprigX {
fmap = sprigMap
for fnNm, fn = range sprigxMap {
fmap[fnNm] = fn
}
} else {
fmap = sprigxMap
for fnNm, fn = range sprigMap {
fmap[fnNm] = fn
}
}
return
}
/*
CombinedHtmlFuncMap returns an [htpl.FuncMap] (like [HtmlFuncMap]) combined with
[github.com/Masterminds/sprig/v3.HtmlFuncMap].
If preferSprigx is true, SprigX function names will override Sprig
functions with the same name.
If false, Sprig functions will override conflicting SprigX functions
with the same name.
*/
func CombinedHtmlFuncMap(preferSprigX bool) (fmap htpl.FuncMap) {
var fn any
var fnNm string
var sprigMap htpl.FuncMap = sprig.HtmlFuncMap()
var sprigxMap htpl.FuncMap = HtmlFuncMap()
if preferSprigX {
fmap = sprigMap
for fnNm, fn = range sprigxMap {
fmap[fnNm] = fn
}
} else {
fmap = sprigxMap
for fnNm, fn = range sprigMap {
fmap[fnNm] = fn
}
}
return
}
/*
CombinedTxtFuncMap returns a [ttpl.FuncMap] (like [TxtFuncMap]) combined with
[github.com/Masterminds/sprig/v3.TxtFuncMap].
If preferSprigx is true, SprigX function names will override Sprig
functions with the same name.
If false, Sprig functions will override conflicting SprigX functions
with the same name.
*/
func CombinedTxtFuncMap(preferSprigX bool) (fmap ttpl.FuncMap) {
var fn any
var fnNm string
var sprigMap ttpl.FuncMap = sprig.TxtFuncMap()
var sprigxMap ttpl.FuncMap = TxtFuncMap()
if preferSprigX {
fmap = sprigMap
for fnNm, fn = range sprigxMap {
fmap[fnNm] = fn
}
} else {
fmap = sprigxMap
for fnNm, fn = range sprigMap {
fmap[fnNm] = fn
}
}
return
}
/*
FuncMap returns a generic function map.
@@ -57,6 +159,11 @@ func HtmlFuncMap() (fmap htpl.FuncMap) {
return
}
// Nop explicitly performs a NO-OP and returns an empty string, allowing one to override "unsafe" functions.
func Nop(obj ...any) (s string) {
return
}
// TxtFuncMap returns a [text/template.FuncMap].
func TxtFuncMap() (fmap ttpl.FuncMap) {
@@ -79,3 +186,171 @@ func TxtFuncMap() (fmap ttpl.FuncMap) {
return
}
/*
toFloat64 uses reflection to resolve any string or numeric type (even custom types) to a float64.
It wraps toString for string types but will fall back to checking numeric types.
If err != nil, then NaN (if true) indicates that:
* val is a string (or pointer to a string), but
* is not a valid numeric string
(you can do this from the caller as well by calling `errors.Is(err, strconv.ErrSyntax)`).
err will always be non-nil if NaN is true.
err will be ErrNilVal if val is nil.
*/
func toFloat64(val any) (f float64, NaN bool, err error) {
var s string
var k reflect.Kind
var rv reflect.Value
// toString will return ErrNilVal if nil.
if s, err = toString(val); err != nil {
if errors.Is(err, ErrBadType) {
// This is OK, it's (hopefully) a number type.
err = nil
} else {
// *probably* ErrNilVal.
return
}
} else {
// We can go ahead and parse this directly since it's already deref'd if a ptr.
if f, err = strconv.ParseFloat(s, 64); err != nil {
NaN = errors.Is(err, strconv.ErrSyntax)
}
// We can return regardless here; it's up to the caller to check NaN/err.
// If they're false/nil, f is parsed already!
return
}
rv = reflect.ValueOf(val)
k = rv.Kind()
if k == reflect.Ptr {
if rv.IsNil() {
// *technically* this should be handled above, but best be safe.
err = ErrNilVal
return
}
rv = rv.Elem()
k = rv.Kind()
}
switch k {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
f = float64(rv.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
f = float64(rv.Uint())
case reflect.Float32, reflect.Float64:
f = rv.Float()
default:
// No need to check for string types since we do that near the beginning.
err = ErrBadType
return
}
return
}
/*
toInt wraps toFloat64, rounds it to the nearest integer,
and converts to an int.
NaN, err have the same meaning as in toFloat64.
This function will panic if float64(val)'s f return exceeds
math.MaxInt on your platform.
*/
func toInt(val any) (i int, NaN bool, err error) {
var f float64
if f, NaN, err = toFloat64(val); err != nil {
return
}
i = int(math.Round(f))
return
}
/*
toPosFloat64 wraps toFloat64 and ensures that it is a positive float64.
NaN, err have the same meaning as in toFloat64.
*/
func toPosFloat64(val any) (f float64, NaN bool, err error) {
if f, NaN, err = toFloat64(val); err != nil {
return
}
f = math.Abs(f)
return
}
/*
toPosInt wraps toPosFloat64, rounds it to the nearest integer,
and converts to an int.
NaN, err have the same meaning as in toPosFloat64 (and thus toFloat64).
This function will panic if float64(val)'s f return exceeds
math.MaxInt on your platform.
*/
func toPosInt(val any) (i int, NaN bool, err error) {
var f float64
if f, NaN, err = toPosFloat64(val); err != nil {
return
}
i = int(math.Round(f))
return
}
/*
toString uses reflection to resolve any string value (even custom types and ptrs)
to a concrete string.
err will be ErrBadType if val is not a string type/string-derived type.
err will be ErrNilVal if val is nil.
*/
func toString(val any) (s string, err error) {
var rv reflect.Value
var k reflect.Kind
if val == nil {
err = ErrNilVal
return
}
rv = reflect.ValueOf(val)
k = rv.Kind()
if k == reflect.Ptr {
if rv.IsNil() {
// *technically* this should be handled above, but best be safe.
err = ErrNilVal
return
}
rv = rv.Elem()
k = rv.Kind()
}
if k == reflect.String {
s = rv.String()
} else {
err = ErrBadType
}
return
}

View File

@@ -1,10 +1,5 @@
package sprigx
// Nop explicitly performs a NO-OP and returns an empty string, allowing one to override "unsafe" functions.
func Nop(obj ...any) (s string) {
return
}
// metaIsNil returns true if obj is explicitly nil.
func metaIsNil(obj any) (isNil bool) {

View File

@@ -0,0 +1,51 @@
package sprigx
import (
`math/big`
)
// numFloat64 returns any string representation of a numeric value or any type of numeric value to a float64.
func numFloat64(val any) (f float64, err error) {
if f, _, err = toFloat64(val); err != nil {
return
}
return
}
/*
numFloatStr wraps numFloat32Str and numFloat64Str.
val can be a string representation of any numeric value or any type of numeric value.
*/
func numFloatStr(val any) (s string, err error) {
var f float64
if f, _, err = toFloat64(val); err != nil {
return
}
s = numFloat64Str(f)
return
}
// numFloat32Str returns float32 f as a complete string representation with no truncation (or right-padding).
func numFloat32Str(f float32) (s string) {
s = numFloat64Str(float64(f))
return
}
// numFloat64Str returns float64 f as a complete string representation with no truncation (or right-padding).
func numFloat64Str(f float64) (s string) {
var bf *big.Float
bf = big.NewFloat(f)
s = bf.String()
return
}

View File

@@ -7,16 +7,29 @@ import (
`strings`
)
// osGroupById returns os/user.LookupGroupId. Can accept either an integer or a string.
func osGroupById[T string | int](gid T) (g *user.Group, err error) {
/*
osGroupById returns os/user.LookupGroupId.
Can accept either a string (`"1000"`) or any
numeric type (`1000`, `-1000`, `1000.0`, `MyCustomType(1000)`, etc.)
*/
func osGroupById(gid any) (g *user.Group, err error) {
var i int
var NaN bool
var gidStr string
switch t := any(gid).(type) {
case string:
gidStr = t
case int:
gidStr = strconv.Itoa(t)
if i, NaN, err = toPosInt(gid); err != nil {
if NaN {
err = nil
if gidStr, err = toString(gid); err != nil {
return
}
} else {
return
}
} else {
gidStr = strconv.Itoa(i)
}
g, err = user.LookupGroupId(gidStr)
@@ -55,16 +68,29 @@ func osHost() (hostNm string, err error) {
return
}
// osUserById returns an os/user.LookupId. Can accept either an integer or a string.
func osUserById[T string | int](uid T) (u *user.User, err error) {
/*
osUserById returns an os/user.LookupId.
Can accept either a string (`"1000"`) or any
numeric type (`1000`, `-1000`, `1000.0`, `MyCustomType(1000)`, etc.)
*/
func osUserById(uid any) (u *user.User, err error) {
var i int
var NaN bool
var uidStr string
switch t := any(uid).(type) {
case string:
uidStr = t
case int:
uidStr = strconv.Itoa(t)
if i, NaN, err = toPosInt(uid); err != nil {
if NaN {
err = nil
if uidStr, err = toString(uid); err != nil {
return
}
} else {
return
}
} else {
uidStr = strconv.Itoa(i)
}
u, err = user.LookupId(uidStr)

View File

@@ -0,0 +1,139 @@
package sprigx
import (
`errors`
`strconv`
`strings`
`time`
)
/*
tmFmt formats time t using format string fstr.
While one certainly can do the same via e.g.
{{- $t := tmNow -}}
{{ $t.Format $fstr }}
This takes a time.Time as the second (and last) parameter,
allowing it to work in pipelines.
*/
func tmFmt(fstr string, t time.Time) (out string) {
out = t.Format(fstr)
return
}
/*
tmParseMonth attempts to first try tmParseMonthInt
and then tries tmParseMonthStr if v is not "numeric".
*/
func tmParseMonth(v any) (mon time.Month, err error) {
var s string
if mon, err = tmParseMonthInt(v); err != nil {
if errors.Is(err, strconv.ErrSyntax) {
// NaN
err = nil
} else {
return
}
}
// If it gets here, it's a non-numeric string.
if s, err = toString(v); err != nil {
return
}
if mon, err = tmParseMonthStr(s); err != nil {
return
}
return
}
/*
tmParseMonthInt parses a number representation of month n to a time.Month.
n may be any numeric type or a string representation of a number
(or a custom type derived from those).
A negative integer (or float, etc.) will be converted to a positive one (e.g. -6 => 6 => time.June).
floats are rounded to the nearest integer.
The integer should map directly to the month constants in the time module:
* 1: January
* 2: February
* 3: March
* 4: April
* 5: May
* 6: June
* 7: July
* 8: August
* 9: September
* 10: October
* 11: November
* 12: December
If n resolves to 0, mon will be the current month (as determined by time.Now).
If n resolves to > 12, err will be ErrBadMonth.
*/
func tmParseMonthInt(n any) (mon time.Month, err error) {
var i int
if i, _, err = toPosInt(n); err != nil {
return
}
if i == 0 {
mon = time.Now().Month()
return
}
if i > 12 {
err = ErrBadMonth
return
}
mon = time.Month(i)
return
}
/*
tmParseMonthStr parses a string representation of month s to a time.Month.
It normalizes s to lowercase and only uses the first 3 characters
(the minimum length needed to determine month name
uniqueness - "June" vs. "July", "March" vs. "May").
An empty (or whitespace-only) string will use the current month (as determined by time.Now).
*/
func tmParseMonthStr(s string) (mon time.Month, err error) {
var i int
var m time.Month
if strings.TrimSpace(s) == "" {
mon = time.Now().Month()
return
}
s = strings.ToLower(strings.TrimSpace(s))[0:3]
for i = range 12 {
m = time.Month(i + 1)
if strings.ToLower(m.String())[0:3] == s {
mon = m
return
}
}
err = ErrBadMonth
return
}