diff --git a/encodingx/binarystr/TODO b/encodingx/binarystr/TODO new file mode 100644 index 0000000..de3ce8a --- /dev/null +++ b/encodingx/binarystr/TODO @@ -0,0 +1 @@ +I need to test this still; I can't recall if it's complete or not diff --git a/encodingx/binarystr/consts.go b/encodingx/binarystr/consts.go new file mode 100644 index 0000000..9ec581e --- /dev/null +++ b/encodingx/binarystr/consts.go @@ -0,0 +1,10 @@ +package binarystr + +const ( + /* + bufSz is the size of the buffer/chunk for encoders/decords to use when unspecified by the method caller. + + The below is 64 KiB, which should be relatively reasonable. + */ + bufSz int = 65536 +) diff --git a/encodingx/binarystr/doc.go b/encodingx/binarystr/doc.go new file mode 100644 index 0000000..6d5bde4 --- /dev/null +++ b/encodingx/binarystr/doc.go @@ -0,0 +1,44 @@ +/* +Package binarystr parses and renders string (and other) representations of binary data. +To distinguish this package from [encoding/binary], this package refers to this string encoding format +as "bit encoding" or "bitstrings". + +For example, the original data: + + []byte("test") + + // Or: + // []byte{0x74, 0x65, 0x73, 0x74} + +can be encoded to a bitstring via e.g. [BytesToStr]: + + 01110100011001010111001101110100 + +And even formatted via [FmtBinStr]: + + // For example, FmtBinStr("01110100011001010111001101110100", "", "", " "): + 01110100 01100101 01110011 01110100 + + // Or FmtBinStr("01110100011001010111001101110100", "", " ", "\n"): + 0111 0100 + 0110 0101 + 0111 0011 + 0111 0100 + +Or can be converted to a series of "binary booleans" via e.g. [BytesToBool]: + + []bool{ + false, true, true, true, + false, true, false, false, + + false, true, true, false, + false, true, false, true, + + false, true, true, true, + false, false, true, true, + + false, true, true, true, + false, true, false, false, + } +*/ +package binarystr diff --git a/encodingx/binarystr/errs.go b/encodingx/binarystr/errs.go new file mode 100644 index 0000000..f03c25a --- /dev/null +++ b/encodingx/binarystr/errs.go @@ -0,0 +1,28 @@ +package binarystr + +import ( + `errors` +) + +var ( + ErrBadNumOct error = errors.New("bad number of bits for even octet split") + ErrNoChunk error = errors.New("empty/nil chunk") +) + +type ( + BadBinCharErr struct { + posByte uint64 + posRune uint64 + r rune + } + + BadNumOctErr struct { + posByte uint64 + posRune uint64 + rem string + } + + BadReadErr struct { + numBytes uint64 + } +) diff --git a/encodingx/binarystr/funcs.go b/encodingx/binarystr/funcs.go new file mode 100644 index 0000000..47a3ca9 --- /dev/null +++ b/encodingx/binarystr/funcs.go @@ -0,0 +1,408 @@ +package binarystr + +import ( + `bytes` + `errors` + `io` + `strconv` + `strings` + `unicode` +) + +/* +BoolToBytes provides the inverse of [BytesToBool]. + +Each boolean in bools indicates a 0 (false) or 1 (true). + +e.g.: + + []bool{ + false, true, true, true, + false, true, false, false, + + false, true, true, false, + false, true, false, true, + + false, true, true, true, + false, false, true, true, + + false, true, true, true, + false, true, false, false, + } + +to: + + []byte{ + 0x74, 0x65, 0x73, 0x74, + } + +If bools is nil, then b, rem, and err will also be nil and this function will no-op. + +If len(bools) % 8 != 0, then err will be [ErrBadNumOct]. +The remaining booleans can be found in rem. +*/ +func BoolToBytes(bools []bool) (b []byte, rem []bool, err error) { + + var idx int + var loc []bool + var sb *strings.Builder = new(strings.Builder) + + if bools == nil { + return + } + if len(bools) == 0 { + b = make([]byte, 0) + return + } + + loc = make([]bool, len(bools)) + copy(loc, bools) + + for idx = 0; idx < len(loc); idx += 8 { + + if len(loc) < 8 { + break + } + + writeBoolChunk(loc[idx:idx+8], sb) + + loc = loc[idx+8:] + } + + if b, err = StrToBytes(sb.String()); err != nil { + return + } + + if len(loc) > 1 { + rem = append(rem, loc...) + err = ErrBadNumOct + } + + return +} + +/* +BytesToStr converts bytes to a bitstring. + +e.g.: + + []byte{ + 0x74, 0x65, 0x73, 0x74, + } + +to: + + "01110100011001010111001101110100" + +For the inverse, see [StrToBytes]. +To format the bitstring, see [FmtBinStr]. +*/ +func BytesToStr(b []byte) (binStr string) { + + var cb byte + var sb *strings.Builder + + if b == nil { + return + } + + sb = new(strings.Builder) + + for _, cb = range b { + // This will never actually return an error. + _, _ = sb.WriteString( + strconv.FormatUint( + uint64(uint8(cb)), + 2, + ), + ) + } + + binStr = sb.String() + + return +} + +/* +BytesToBool returns a slice of "binary booleans". + +For each bit of data in b, a 0 is false and a 1 is true (as one would expect). + +For the inverse of this, see [BoolToBytes]. +*/ +func BytesToBool(b []byte) (bools []bool) { + + var err error + var r rune + var rSz int + var idx int + var s *strings.Reader + + if len(b) == 0 { + return + } + + // s will always be a valid bitstring. + s = strings.NewReader(BytesToStr(b)) + + bools = make([]bool, s.Len()) + + for idx = 0; idx < s.Len(); idx++ { + r, rSz, err = s.ReadRune() + + if rSz == 0 { + break + } + if err != nil { + // It'll only be an io.EOF if anything at all. + err = nil + break + } + + switch r { + case '0': + bools[idx] = false + case '1': + bools[idx] = true + } + } + + return +} + +/* +FmtBinStr formats bitstring s to more comfortable representations. + +bitSep is a string to separate each bit boundary. + +nybSep is a string to separate each "nibble"/"nybble" (4-bit "word") boundary. + +bytSep is a string to separate each byte boundary. + +Note that if you only explicitly use whitespace separators, then the output +can still be parsed back to native []byte{} data via [StrToBytes]. + +Note that NO checking is done against the validity of s being a bitstring; +the only content-checking done in s is if a given rune is a whitespace character or not +(whitespace present in s is skipped over, which lets you run FmtBinStr to reformat a bitstring). +*/ +func FmtBinStr(s, bitSep, nybSep, bytSep string) (out string) { + + var idx int + var lastIdx int + var sb *strings.Builder + + if strings.TrimSpace(s) == "" { + return + } + + sb = new(strings.Builder) + + lastIdx = len(s) - 1 + + for idx = 0; idx < len(s); idx++ { + _ = sb.WriteByte(s[idx]) + if idx != lastIdx { + if (idx+1)%8 == 0 { + if bytSep != "" { + _, _ = sb.WriteString(bytSep) + } + continue + } else if (idx+1)%4 == 0 { + if nybSep != "" { + _, _ = sb.WriteString(nybSep) + } + continue + } + } + if bitSep != "" { + _, _ = sb.WriteString(bitSep) + } + } + + out = sb.String() + + return +} + +/* +NewDecoder returns a [Decoder] from [io.Reader] r. + +Data from r is expected to be BITSTRING data (strings); +doing read operations from a [Decoder] returns PLAIN data. +*/ +func NewDecoder(r io.Reader) (e *Decoder) { + + e = &Decoder{ + r: r, + } + + return +} + +/* +NewEncoder returns an [Encoder] from [io.Reader] r. + +Data from r is expected to be PLAIN data; +doing read operations from an [Encoder] returns BITSTRING data (strings). +*/ +func NewEncoder(r io.Reader) (e *Encoder) { + + e = &Encoder{ + r: r, + } + + return +} + +/* +StrToBytes converts a bitstring to bytes. + +Whitespace is ignored. + +e.g.: + + "01110100011001010111001101110100" + +to: + + []byte("test") + +or: + + []byte{0x74, 0x65, 0x73, 0x74} + +For the inverse, see [BytesToStr]. + +If the number of characters in binStr is not modulo 8 (`len() % 8 != 0`), +then err will be of type [BadNumOctErr]. + +If there are any characters encountered in binStr that are not "0" or "1" +(other than whitespace), then err will be of type [BadBinCharErr]. +*/ +func StrToBytes(binStr string) (b []byte, err error) { + + var r rune + var cb byte + var rSz int + var pos uint64 + var rErr error + var posR uint64 + var chunk []rune + var buf *strings.Reader + var bytBuf *bytes.Buffer = new(bytes.Buffer) + + defer func() { + b = bytBuf.Bytes() + }() + + if strings.TrimSpace(binStr) == "" { + return + } + buf = strings.NewReader(binStr) + + chunk = make([]rune, 0, 8) + for { + r, rSz, rErr = buf.ReadRune() + if rSz == 0 { + break + } + pos += uint64(rSz) + if rSz != 0 { + posR++ + } + if len(chunk) == 8 { + if cb, err = chunkToByte(chunk); err != nil { + return + } + if err = bytBuf.WriteByte(cb); err != nil { + return + } + chunk = make([]rune, 0, 8) + } + if unicode.IsSpace(r) { + continue + } + if rSz != 0 { + switch r { + case '0', '1': + chunk = append(chunk, r) + default: + err = &BadBinCharErr{ + posByte: pos, + posRune: posR, + r: r, + } + return + } + } + if rErr != nil { + if errors.Is(rErr, io.EOF) { + break + } else { + err = rErr + return + } + } + if rSz == 0 { + break + } + } + + if len(chunk) != 0 { + if len(chunk) == 8 { + if cb, err = chunkToByte(chunk); err != nil { + return + } + if err = bytBuf.WriteByte(cb); err != nil { + return + } + } else { + err = &BadNumOctErr{ + posByte: pos, + posRune: posR, + rem: string(chunk), + } + return + } + } + + return +} + +func chunkToByte(chunk []rune) (b byte, err error) { + + var u8 uint64 + + if len(chunk) == 0 { + err = ErrNoChunk + return + } + + if u8, err = strconv.ParseUint(string(chunk), 2, 8); err != nil { + return + } + + b = byte(uint8(u8)) + + return +} + +func writeBoolChunk(chunk []bool, sb io.StringWriter) { + + var b bool + + if len(chunk) == 0 { + return + } + + for _, b = range chunk { + switch b { + case false: + sb.WriteString("0") + case true: + sb.WriteString("1") + } + } + +} diff --git a/encodingx/binarystr/funcs_badbincharerr.go b/encodingx/binarystr/funcs_badbincharerr.go new file mode 100644 index 0000000..03676b4 --- /dev/null +++ b/encodingx/binarystr/funcs_badbincharerr.go @@ -0,0 +1,26 @@ +package binarystr + +import "fmt" + +// BytePos returns the byte position index immediately leading up to the error. +func (b *BadBinCharErr) BytePos() (pos uint64) { return b.posByte } + +// Rune returns the offending rune that caused the error. +func (b *BadBinCharErr) Rune() (r rune) { return b.r } + +// RunePos returns the rune (character) position index immediately leading up to the error. +func (b *BadBinCharErr) RunePos() (pos uint64) { return b.posRune } + +// Error returns the error message for a BadBinCharError. It conforms to an [error] interface. +func (b *BadBinCharErr) Error() (err string) { + + err = fmt.Sprintf( + "rune/character after position index #%d (%#v) (byte #%d) is not \"0\" or \"1\"", + b.posRune, b.r, b.posByte, + ) + + return +} + +// String is an alias for the [BadBinCharErr.Error] method to conform explicitly to a [fmt.Stringer] interface. +func (b *BadBinCharErr) String() (s string) { return b.Error() } diff --git a/encodingx/binarystr/funcs_badnumocterr.go b/encodingx/binarystr/funcs_badnumocterr.go new file mode 100644 index 0000000..13ca50e --- /dev/null +++ b/encodingx/binarystr/funcs_badnumocterr.go @@ -0,0 +1,32 @@ +package binarystr + +import "fmt" + +// BytePos returns the byte position index immediately leading up to the error. +func (b *BadNumOctErr) BytePos() (pos uint64) { return b.posByte } + +// Error returns the error message for a BadNumOctError. It conforms to an [error] interface. +func (b *BadNumOctErr) Error() (err string) { + + err = fmt.Sprintf( + "only able to read %d complete octets/bytes (%#d runes/characters); "+ + "%d remaining characters are less than 8 bits", + b.posByte, b.posRune, + len(b.rem), + ) + + return +} + +/* +Rem returns the remaining unparsed string of characters; this should ALWAYS be a length of 0 <= len(rem) < 8. +(It will only be an empty string if [Decoder.Read] encountered the end of its source reader before the buffer was filled, +but the available data cleanly broke into a last octet.) +*/ +func (b *BadNumOctErr) Rem() (rem string) { return b.rem } + +// RunePos returns the rune (character) position index immediately leading up to the error. +func (b *BadNumOctErr) RunePos() (pos uint64) { return b.posRune } + +// String is an alias for the [BadNumOctErr.Error] method to conform explicitly to a [fmt.Stringer] interface. +func (b *BadNumOctErr) String() (s string) { return b.Error() } diff --git a/encodingx/binarystr/funcs_badreaderr.go b/encodingx/binarystr/funcs_badreaderr.go new file mode 100644 index 0000000..b876172 --- /dev/null +++ b/encodingx/binarystr/funcs_badreaderr.go @@ -0,0 +1,16 @@ +package binarystr + +import ( + `fmt` +) + +// Error conforms a [BadReadErr] to an [error]. +func (b *BadReadErr) Error() (err string) { + + err = fmt.Sprintf("only read %d bytes from internal read source before encountering a read error", b.numBytes) + + return +} + +// NumBytesRead returns the number of bytes read (BEFORE mutation/encoding/decoding) before the error was hit. +func (b *BadReadErr) NumBytesRead() (u64 uint64) { return b.numBytes } diff --git a/encodingx/binarystr/funcs_decoder.go b/encodingx/binarystr/funcs_decoder.go new file mode 100644 index 0000000..990f273 --- /dev/null +++ b/encodingx/binarystr/funcs_decoder.go @@ -0,0 +1,116 @@ +package binarystr + +import ( + `errors` + `io` + `strings` +) + +/* +Read conforms to an [io.Reader]. +It returns plain data decoded from bitstring encoding in e's read source. + +IMPORTANT: Read will return a [BadNumOctErr] if the amount of data in e's read source is not at least len(b) * 8! +ONE BYTE in b equals EIGHT BYTES from e's read source! +Likewise, n will ALWAYS be len(b), but the number of bytes actually read from e's internal reader +will ALWAYS be n*8! +In this case, b will still be populated with the available decoded data, and err ([BadNumOctErr])'s +[BadNumOctErr.Rem] method will contain the remaining bitstring (if there is any left over) after decoding +the octets that were returned (if any). + +IMPORTANT: Read will return a [BadBinCharErr] if it encounters any character in e's read source that is not '0' or '1'! +THIS INCLUDES WHITESPACE due to the buffer size calculation (unlike [StrToBytes]); it is NOT skipped! +*/ +func (e *Decoder) Read(b []byte) (n int, err error) { + + var inN int + var s string + var endIdx int + var rem string + var buf []byte + + defer func() { + if b != nil && buf != nil { + s = BytesToStr(buf) + copy(b, []byte(s)) + } + }() + + if len(b) == 0 { + return + } + + buf = make([]byte, len(b)*8) + if inN, err = e.r.Read(buf); err != nil { + // This is caught/handled below. + if errors.Is(err, io.EOF) { + err = nil + } else { + return + } + } + + if inN < len(buf) { + + } + + if inN != len(buf) { + err = &BadNumOctErr{ + posByte: uint64(inN), + posRune: uint64(inN), + rem: rem, // We hit the end too "early" so there is no "remaining". + } + return + } + + return +} + +/* +ReadString returns a bitstring-encoded [string] from the entirety of the data +in the source reader of e. + +IMPORTANT: If e's source reader is a non-terminating stream (e.g. a [math/rand.Rand]), +this function will NEVER RETURN (and will likely consume all memory). +YOU HAVE BEEN WARNED. + +n is the number of bytes read from e's source reader. +*/ +func (e *Decoder) ReadString() (s string, n uint64, err error) { + + var inN int + var buf []byte + var rErr error + var sb *strings.Builder = new(strings.Builder) + + defer func() { + if sb != nil { + s = sb.String() + } + }() + + for { + buf = make([]byte, bufSz) + inN, rErr = e.r.Read(buf) + n += uint64(inN) + + if inN != 0 { + if inN < bufSz { + buf = buf[:inN] + } + sb.WriteString(BytesToStr(buf)) + } else { + if errors.Is(rErr, io.EOF) { + rErr = nil + } + break + } + + if rErr != nil { + err = rErr + return + } + } + + return +} diff --git a/encodingx/binarystr/funcs_encoder.go b/encodingx/binarystr/funcs_encoder.go new file mode 100644 index 0000000..bc9b432 --- /dev/null +++ b/encodingx/binarystr/funcs_encoder.go @@ -0,0 +1,105 @@ +package binarystr + +import ( + `errors` + `io` + `strings` +) + +/* +Read conforms to an [io.Reader]. +It returns bitstring encoding of the data in e's read source. + +(To conform to [io.Reader], this is returned as bytes - e.g. []byte(string()).) + +IMPORTANT: Read will return [ErrBadNumOct] if len(b) % 8 != 0! EIGHT BYTES in b equal ONE BYTE +from e's read source! Keep this in mind and size b accordingly for your desired decoded data size! +Likewise, n will ALWAYS be len(b), but the number of bytes actually read from e's internal reader +will ALWAYS be n/8! (If for some bizarre case it isn't, err will be [BadReadErr].) +*/ +func (e *Encoder) Read(b []byte) (n int, err error) { + + var inN int + var s string + var buf []byte + + defer func() { + if b != nil && buf != nil { + s = BytesToStr(buf) + copy(b, []byte(s)) + } + }() + + if len(b) == 0 { + return + } + if len(b)%8 != 0 { + err = ErrBadNumOct + return + } + + buf = make([]byte, len(b)/8) + if inN, err = e.r.Read(buf); err != nil { + return + } + if inN != len(buf) { + if len(buf) > inN { + buf = buf[:inN] + } + err = &BadReadErr{ + numBytes: uint64(inN), + } + return + } + + return +} + +/* +ReadString returns a bitstring-encoded [string] from the entirety of the data +in the source reader of e. + +IMPORTANT: If e's source reader is a non-terminating stream (e.g. a [math/rand.Rand]), +this function will NEVER RETURN (and will likely consume all memory). +YOU HAVE BEEN WARNED. + +n is the number of bytes read from e's source reader. +*/ +func (e *Encoder) ReadString() (s string, n uint64, err error) { + + var inN int + var buf []byte + var rErr error + var sb *strings.Builder = new(strings.Builder) + + defer func() { + if sb != nil { + s = sb.String() + } + }() + + for { + buf = make([]byte, bufSz) + inN, rErr = e.r.Read(buf) + n += uint64(inN) + + if inN != 0 { + if inN < bufSz { + buf = buf[:inN] + } + sb.WriteString(BytesToStr(buf)) + } else { + if errors.Is(rErr, io.EOF) { + rErr = nil + } + break + } + + if rErr != nil { + err = rErr + return + } + } + + return +} diff --git a/encodingx/binarystr/types.go b/encodingx/binarystr/types.go new file mode 100644 index 0000000..973197c --- /dev/null +++ b/encodingx/binarystr/types.go @@ -0,0 +1,17 @@ +package binarystr + +import ( + `io` +) + +type ( + Encoder struct { + r io.Reader + cur uint64 + } + + Decoder struct { + r io.Reader + cur uint64 + } +) diff --git a/go.mod b/go.mod index c4007af..73109dd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module r00t2.io/goutils -go 1.26 +go 1.27 require ( github.com/Masterminds/sprig/v3 v3.3.0 @@ -8,10 +8,12 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/deckarep/golang-set/v2 v2.9.0 github.com/google/uuid v1.6.0 + github.com/miekg/dns v1.1.73 github.com/olekukonko/tablewriter v1.1.4 - github.com/shirou/gopsutil/v4 v4.26.6 - go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/sys v0.47.0 + github.com/shirou/gopsutil/v4 v4.26.9 + go4.org/netipx v0.0.0-20260823151212-3075585bcbeb + golang.org/x/sys v0.49.0 + r00t2.io/subnetter v0.3.1 r00t2.io/sysutils v1.16.5 ) @@ -23,12 +25,19 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/djherbis/times v1.6.0 // indirect - github.com/ebitengine/purego v0.10.2 // indirect + github.com/ebitengine/purego v0.11.1 // indirect github.com/fatih/color v1.19.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.15 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-playground/locales v0.14.2 // indirect + github.com/go-playground/universal-translator v0.18.2 // indirect + github.com/go-playground/validator/v10 v10.30.5 // indirect + github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/goccy/go-json v0.10.6 // indirect - github.com/huandu/xstrings v1.5.0 // indirect - github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/huandu/xstrings v1.6.2 // indirect + github.com/leodido/go-urn v1.5.0 // indirect + github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.24 // indirect @@ -37,13 +46,15 @@ require ( github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.3.0 // indirect github.com/olekukonko/ll v0.1.8 // indirect - github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shopspring/decimal v1.4.0 // indirect + github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0 // indirect + github.com/shopspring/decimal v1.5.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/tklauser/go-sysconf v0.4.0 // indirect github.com/tklauser/numcpus v0.12.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.mongodb.org/mongo-driver v1.17.9 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/sync v0.22.0 // indirect + go.mongodb.org/mongo-driver v1.17.10 // indirect + golang.org/x/crypto v0.58.0 // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/sync v0.24.0 // indirect + golang.org/x/text v0.43.0 // indirect ) diff --git a/go.sum b/go.sum index bebec67..05fdf81 100644 --- a/go.sum +++ b/go.sum @@ -30,25 +30,43 @@ github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/ github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE= github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.11.1 h1:2zpWRSQNVKN4eKsKO9eM1ILDgWfYMY9GwqRmK6XeQ/0= +github.com/ebitengine/purego v0.11.1/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= +github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/locales v0.14.2 h1:d8UmcrM6Nip0hfGZKLGpAvZH37XB4TS0xzK9B56YNCY= +github.com/go-playground/locales v0.14.2/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.2 h1:LCsMLC9RzmbUMNUPVYD15dmcjwYAJhmX8mPZRW4rAVU= +github.com/go-playground/universal-translator v0.18.2/go.mod h1:67VZIMp5lQpDWlnStOct22q1bkdJGJqHghbOtmkawxk= +github.com/go-playground/validator/v10 v10.30.5 h1:YyCXvVShZbs2Sm3Mb53eNOlhRXctSOzW5QJAouCTZL4= +github.com/go-playground/validator/v10 v10.30.5/go.mod h1:wEqiaov48pXX1kjhc3Da8y0M0Dtg/BK7gurFBLgwFrQ= +github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= +github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.6.2 h1:+X5X6N46b40cmDw7FFJFU6Eoq0yJS8lbYigT2EFau4c= +github.com/huandu/xstrings v1.6.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0= +github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= @@ -57,12 +75,16 @@ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRt github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15 h1:YkjVPl/YH5XlJ+/NiwzJtPYXXKRcyjmEUhsDci6YK3c= github.com/lufia/plan9stats v0.0.0-20260627054121-477a66015f15/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0= +github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -79,6 +101,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0 h1:XA01Vk/wv9YikCi1V51yRzIHPMT5of9+cMpZoZvDn/M= +github.com/power-devops/perfstat v0.0.0-20260916203055-22a1a467d9f0/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY= @@ -89,12 +113,17 @@ github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9R github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs= github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/shirou/gopsutil/v4 v4.26.9 h1:CaBo/hBFqvlJoLvAQEKdHdf5GD97/MN2ACfyv63+2Ig= +github.com/shirou/gopsutil/v4 v4.26.9/go.mod h1:nKH+8wX2zxr/mDbsR+AAy6Qd2z4ZOQ7wfQaEp+yrYmg= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shopspring/decimal v1.5.0 h1:HxSWk31RmJv0EyY4oLiGQB9u7WPludUwPBdvQZnYFPc= +github.com/shopspring/decimal v1.5.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU= @@ -107,8 +136,12 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.mongodb.org/mongo-driver v1.17.10 h1:kdAgQvu8TROXZpSkJQd5wzfaNCCrMbpZyKFtQ6qkPCE= +go.mongodb.org/mongo-driver v1.17.10/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +go4.org/netipx v0.0.0-20260823151212-3075585bcbeb h1:XBM4hvfwGAttkkiTIFfeigdfcL1xIfdKXqFdgiHGtDs= +go4.org/netipx v0.0.0-20260823151212-3075585bcbeb/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= @@ -117,12 +150,18 @@ golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -135,8 +174,14 @@ golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +r00t2.io/subnetter v0.3.1 h1:I/Uoit7Id+QaFK2W2GwUdkr6WopcagFWxCur1gUnTdc= +r00t2.io/subnetter v0.3.1/go.mod h1:nyOpA8S1IU91gcaP8DMaq/VBUHJ28luGp+ulQFsv6YM= r00t2.io/sysutils v1.16.2 h1:wI01UwZ/bXn/lzBiCpqDmzZCOWiK87kz04SB4xRw+W0= r00t2.io/sysutils v1.16.2/go.mod h1:iXK+ALOwIdRKjAJIE5USlkZ669SVDHBNNuYhunsznH8= r00t2.io/sysutils v1.16.5 h1:0v/HWTLOl8UN8B6koXM3e+TWN1N3eAoUM+xGK+itwq0= diff --git a/mapsx/funcs.go b/mapsx/funcs.go index ff91c94..05d08dc 100644 --- a/mapsx/funcs.go +++ b/mapsx/funcs.go @@ -1,5 +1,11 @@ package mapsx +import ( + "cmp" + "iter" + "slices" +) + /* Get mimics Python's [dict.get()] behavior, returning value `v` if key `k` is not found in map `m`. @@ -34,6 +40,7 @@ if `m` is non-nil but empty. The ordering of `mapKeys` is unpredictable as maps are unsorted and no explicit sorting is performed. +See [SortedKeys] and [ListSortedKeys] if sorting is desired. */ func ListKeys[Map ~map[K]V, K comparable, V any](m Map) (mapKeys []K) { @@ -51,6 +58,36 @@ func ListKeys[Map ~map[K]V, K comparable, V any](m Map) (mapKeys []K) { return } +/* +ListSortedKeys is like [ListKeys] but returns the keys in a sorted slice. + +The keys in map `m` MUST conform to a [cmp.Ordered]! +*/ +func ListSortedKeys[Map ~map[K]V, K cmp.Ordered, V any](m Map) (mapKeys []K) { + + // All cmp.Ordered types are comparable, but not all comparable types are cmp.Ordered. + mapKeys = ListKeys(m) + + slices.Sort(mapKeys) + + return +} + +/* +ListSortedValues is like [ListValues] but returns the values in a sorted slice. + +The values in map `m` MUST conform to a [cmp.Ordered]! +*/ +func ListSortedValues[Map ~map[K]V, K comparable, V cmp.Ordered](m Map) (mapVals []V) { + + // All cmp.Ordered types are comparable, but not all comparable types are cmp.Ordered. + mapVals = ListValues(m) + + slices.Sort(mapVals) + + return +} + /* ListValues returns map `m`'s values as a slice (unlike [maps.Values], which returns an iterator via [iter.Seq]). @@ -60,6 +97,7 @@ if `m` is non-nil but empty. The ordering of `mapVals` is unpredictable as maps are unsorted and no explicit sorting is performed. +See [SortedValues] and [ListSortedValues] if sorting is desired. */ func ListValues[Map ~map[K]V, K comparable, V any](m Map) (mapVals []V) { @@ -77,6 +115,52 @@ func ListValues[Map ~map[K]V, K comparable, V any](m Map) (mapVals []V) { return } +/* +SortedKeys is like [maps.Keys] but will sort the keys in the returned [iter.Seq]. +(There is a small cost to this.) + +The keys in map `m` MUST conform to a [cmp.Ordered]! +*/ +func SortedKeys[Map ~map[K]V, K cmp.Ordered, V any](m Map) (mapKeys iter.Seq[K]) { + + var k K + // All cmp.Ordered types are comparable, but not all comparable types are cmp.Ordered. + var keys []K = ListKeys(m) + + slices.Sort(keys) + + return func(yield func(K) bool) { + for _, k = range keys { + if !yield(k) { + return + } + } + } +} + +/* +SortedValues is like [maps.Values] but will sort the values in the returned [iter.Seq]. +(There is a small cost to this.) + +The values in map `m` MUST conform to a [cmp.Ordered]! +*/ +func SortedValues[Map ~map[K]V, K comparable, V cmp.Ordered](m Map) (mapVals iter.Seq[V]) { + + var v V + // All cmp.Ordered types are comparable, but not all comparable types are cmp.Ordered. + var vals []V = ListValues(m) + + slices.Sort(vals) + + return func(yield func(V) bool) { + for _, v = range vals { + if !yield(v) { + return + } + } + } +} + /* Must, unlike [Get] or [GetOk], requires that `k` be in map `m`. diff --git a/tplx/sprigx/README.html b/tplx/sprigx/README.html index 65a938a..cfae385 100644 --- a/tplx/sprigx/README.html +++ b/tplx/sprigx/README.html @@ -636,7 +636,7 @@ pre.rouge .gs {