Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f4316cc13a | ||
|
19860f713c |
10
.drone.yml
Normal file
10
.drone.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: golang
|
||||||
|
commands:
|
||||||
|
- go test ./...
|
||||||
|
- go build ./...
|
@@ -1,18 +0,0 @@
|
|||||||
name: Go
|
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go-version: [ '1.23' ]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup Go ${{ matrix.go-version }}
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go-version }}
|
|
||||||
- name: Build
|
|
||||||
run: go build -v ./...
|
|
||||||
- name: Test
|
|
||||||
run: go test -v ./...
|
|
19
Makefile
19
Makefile
@@ -1,19 +0,0 @@
|
|||||||
# Variables
|
|
||||||
REPO_REMOTE := origin
|
|
||||||
|
|
||||||
# Helper function to get the latest tag and increment the version
|
|
||||||
define next_version
|
|
||||||
latest_tag=$$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0"); \
|
|
||||||
major=$$(echo $$latest_tag | sed -E 's/^v([0-9]+)\.[0-9]+\.[0-9]+/\1/'); \
|
|
||||||
minor=$$(echo $$latest_tag | sed -E 's/^v[0-9]+\.([0-9]+)\.[0-9]+/\1/'); \
|
|
||||||
patch=$$(echo $$latest_tag | sed -E 's/^v[0-9]+\.[0-9]+\.([0-9]+)/\1/'); \
|
|
||||||
next_patch=$$(($$patch + 1)); \
|
|
||||||
echo "v$$major.$$minor.$$next_patch"
|
|
||||||
endef
|
|
||||||
|
|
||||||
# Target to tag and push the next version
|
|
||||||
release:
|
|
||||||
next_version=$$($(next_version)); \
|
|
||||||
echo "Tagging with version $$next_version"; \
|
|
||||||
git tag $$next_version; \
|
|
||||||
git push $(REPO_REMOTE) $$next_version
|
|
@@ -7,7 +7,7 @@ A mix of useful packages, some are works in progress.
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```
|
```
|
||||||
go get git.sdf.org/jchenry/x
|
go get github.com/jchenry/x
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
3
cache/doc.go
vendored
3
cache/doc.go
vendored
@@ -1,3 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
// Cache package cribbed from https://varunksaini.com/tiered-cache-in-go/ whom i have i high respect for as a former colleague.
|
|
6
cache/interface.go
vendored
6
cache/interface.go
vendored
@@ -1,6 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
type Interface[K comparable, V any] interface {
|
|
||||||
Get(key K) V
|
|
||||||
Put(key K, value V)
|
|
||||||
}
|
|
40
cache/tiered.go
vendored
40
cache/tiered.go
vendored
@@ -1,40 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"git.sdf.org/jchenry/x"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tieredCache[K comparable, V any] struct {
|
|
||||||
inner Interface[K, V]
|
|
||||||
outer Interface[K, V]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTieredCache[K comparable, V any](inner, outer Interface[K, V]) Interface[K, V] {
|
|
||||||
x.Assert(inner != nil, "cache.NewTieredCache: inner cannot be nil")
|
|
||||||
x.Assert(outer != nil, "cache.NewTieredCache: outer cannot be nil")
|
|
||||||
return &tieredCache[K, V]{
|
|
||||||
inner: inner,
|
|
||||||
outer: outer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tieredCache[K, V]) Get(key K) V {
|
|
||||||
var zero, value V
|
|
||||||
value = t.inner.Get(key)
|
|
||||||
if reflect.DeepEqual(value, zero) {
|
|
||||||
value = t.outer.Get(key)
|
|
||||||
// if required, add value to inner cache for future requests
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tieredCache[K, V]) Put(key K, value V) {
|
|
||||||
t.inner.Put(key, value)
|
|
||||||
|
|
||||||
// add key to outer cache asynchronously
|
|
||||||
go func(key K) {
|
|
||||||
t.outer.Put(key, value)
|
|
||||||
}(key)
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
package sort
|
|
||||||
|
|
||||||
// Node is a generic interface representing a graph node.
|
|
||||||
type Node[T any] interface {
|
|
||||||
// Value returns the value of the node.
|
|
||||||
Value() T
|
|
||||||
// Adjacencies returns a slice of nodes adjacent to this node
|
|
||||||
Adjacencies() []Node[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Topo performs a topological sort on a slice of nodes in place.
|
|
||||||
func TopoSort[T any](nodes []Node[T]) {
|
|
||||||
v := make(map[Node[T]]bool)
|
|
||||||
pos := 0
|
|
||||||
var dfs func(Node[T])
|
|
||||||
dfs = func(n Node[T]) {
|
|
||||||
v[n] = true
|
|
||||||
for _, e := range n.Adjacencies() {
|
|
||||||
if !v[e] {
|
|
||||||
dfs(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nodes[pos] = n
|
|
||||||
pos++
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range nodes {
|
|
||||||
if !v[n] {
|
|
||||||
dfs(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RTopoSort performs a reverse topological sort on a slice of nodes in place.
|
|
||||||
func RTopoSort[T any](nodes []Node[T]) {
|
|
||||||
TopoSort(nodes)
|
|
||||||
|
|
||||||
for i, j := 0, len(nodes)-1; i < j; i, j = i+1, j-1 {
|
|
||||||
nodes[i], nodes[j] = nodes[j], nodes[i]
|
|
||||||
}
|
|
||||||
}
|
|
28
container/set/interface.go
Normal file
28
container/set/interface.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
var x = struct{}{}
|
||||||
|
|
||||||
|
type Set map[any]struct{}
|
||||||
|
|
||||||
|
func (s *Set) Init() {
|
||||||
|
for k := range *s {
|
||||||
|
delete(*s, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Set) Add(e any) {
|
||||||
|
(*s)[e] = x
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Set) Remove(e any) {
|
||||||
|
delete(*s, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Set) Contains(e any) bool {
|
||||||
|
_, c := (*s)[e]
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Set {
|
||||||
|
return new(Set)
|
||||||
|
}
|
@@ -1,101 +0,0 @@
|
|||||||
package trie
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Node represents a node in the trie
|
|
||||||
type Node struct {
|
|
||||||
children map[rune]*Node
|
|
||||||
isEnd bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new empty trie root node
|
|
||||||
func New() *Node {
|
|
||||||
return &Node{
|
|
||||||
children: make(map[rune]*Node),
|
|
||||||
isEnd: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds a word to the trie
|
|
||||||
func (n *Node) Add(word string) {
|
|
||||||
current := n
|
|
||||||
for _, char := range word {
|
|
||||||
if _, exists := current.children[char]; !exists {
|
|
||||||
current.children[char] = &Node{
|
|
||||||
children: make(map[rune]*Node),
|
|
||||||
isEnd: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
current = current.children[char]
|
|
||||||
}
|
|
||||||
current.isEnd = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// countWords recursively counts the number of words in the subtree
|
|
||||||
func (n *Node) countWords() int {
|
|
||||||
count := 0
|
|
||||||
if n.isEnd {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
for _, child := range n.children {
|
|
||||||
count += child.countWords()
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count returns the number of words that have the given prefix
|
|
||||||
func (n *Node) Count(prefix string) int {
|
|
||||||
current := n
|
|
||||||
for _, char := range prefix {
|
|
||||||
if _, exists := current.children[char]; !exists {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
current = current.children[char]
|
|
||||||
}
|
|
||||||
return current.countWords()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrefixCount represents a prefix and its count
|
|
||||||
type PrefixCount struct {
|
|
||||||
Prefix string
|
|
||||||
Count int
|
|
||||||
}
|
|
||||||
|
|
||||||
// collectPrefixes recursively collects all prefixes and their counts
|
|
||||||
func collectPrefixes(n *Node, currentPrefix string, results *[]PrefixCount) {
|
|
||||||
count := n.countWords()
|
|
||||||
if count > 0 {
|
|
||||||
*results = append(*results, PrefixCount{
|
|
||||||
Prefix: currentPrefix,
|
|
||||||
Count: count,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Sort children by rune to ensure consistent traversal order
|
|
||||||
chars := make([]rune, 0, len(n.children))
|
|
||||||
for char := range n.children {
|
|
||||||
chars = append(chars, char)
|
|
||||||
}
|
|
||||||
sort.Slice(chars, func(i, j int) bool {
|
|
||||||
return chars[i] < chars[j]
|
|
||||||
})
|
|
||||||
for _, char := range chars {
|
|
||||||
collectPrefixes(n.children[char], currentPrefix+string(char), results)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TopPrefixes returns a sorted list of prefixes by their counts
|
|
||||||
func TopPrefixes(n *Node) []PrefixCount {
|
|
||||||
var results []PrefixCount
|
|
||||||
collectPrefixes(n, "", &results)
|
|
||||||
|
|
||||||
sort.Slice(results, func(i, j int) bool {
|
|
||||||
if results[i].Count == results[j].Count {
|
|
||||||
return results[i].Prefix < results[j].Prefix
|
|
||||||
}
|
|
||||||
return results[i].Count > results[j].Count
|
|
||||||
})
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
@@ -1,163 +0,0 @@
|
|||||||
package trie
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
|
||||||
root := New()
|
|
||||||
if root == nil {
|
|
||||||
t.Error("New() returned nil")
|
|
||||||
}
|
|
||||||
if root.children == nil {
|
|
||||||
t.Error("New() root node has nil children map")
|
|
||||||
}
|
|
||||||
if root.isEnd {
|
|
||||||
t.Error("New() root node should not be marked as end")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddAndCount(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
words []string
|
|
||||||
prefix string
|
|
||||||
expected int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty trie",
|
|
||||||
words: []string{},
|
|
||||||
prefix: "test",
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single word",
|
|
||||||
words: []string{"hello"},
|
|
||||||
prefix: "hel",
|
|
||||||
expected: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple words same prefix",
|
|
||||||
words: []string{"hello", "help", "hell"},
|
|
||||||
prefix: "hel",
|
|
||||||
expected: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "prefix not found",
|
|
||||||
words: []string{"hello", "help", "hell"},
|
|
||||||
prefix: "xyz",
|
|
||||||
expected: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty prefix",
|
|
||||||
words: []string{"hello", "help", "hell"},
|
|
||||||
prefix: "",
|
|
||||||
expected: 3,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
root := New()
|
|
||||||
for _, word := range tt.words {
|
|
||||||
root.Add(word)
|
|
||||||
}
|
|
||||||
if got := root.Count(tt.prefix); got != tt.expected {
|
|
||||||
t.Errorf("Count(%q) = %d, want %d", tt.prefix, got, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTopPrefixes(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
words []string
|
|
||||||
expected []PrefixCount
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty trie",
|
|
||||||
words: []string{},
|
|
||||||
expected: []PrefixCount{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single word",
|
|
||||||
words: []string{"hello"},
|
|
||||||
expected: []PrefixCount{
|
|
||||||
{Prefix: "", Count: 1},
|
|
||||||
{Prefix: "h", Count: 1},
|
|
||||||
{Prefix: "he", Count: 1},
|
|
||||||
{Prefix: "hel", Count: 1},
|
|
||||||
{Prefix: "hell", Count: 1},
|
|
||||||
{Prefix: "hello", Count: 1},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple words",
|
|
||||||
words: []string{"hello", "help", "hell", "helicopter"},
|
|
||||||
expected: []PrefixCount{
|
|
||||||
{Prefix: "", Count: 4},
|
|
||||||
{Prefix: "h", Count: 4},
|
|
||||||
{Prefix: "he", Count: 4},
|
|
||||||
{Prefix: "hel", Count: 4},
|
|
||||||
{Prefix: "hell", Count: 2},
|
|
||||||
{Prefix: "hello", Count: 1},
|
|
||||||
{Prefix: "help", Count: 1},
|
|
||||||
{Prefix: "heli", Count: 1},
|
|
||||||
{Prefix: "helic", Count: 1},
|
|
||||||
{Prefix: "helico", Count: 1},
|
|
||||||
{Prefix: "helicop", Count: 1},
|
|
||||||
{Prefix: "helicopt", Count: 1},
|
|
||||||
{Prefix: "helicopte", Count: 1},
|
|
||||||
{Prefix: "helicopter", Count: 1},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
root := New()
|
|
||||||
for _, word := range tt.words {
|
|
||||||
root.Add(word)
|
|
||||||
}
|
|
||||||
got := TopPrefixes(root)
|
|
||||||
if len(got) != len(tt.expected) {
|
|
||||||
t.Errorf("TopPrefixes() returned %d results, want %d", len(got), len(tt.expected))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for i, want := range tt.expected {
|
|
||||||
if i >= len(got) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if got[i].Prefix != want.Prefix || got[i].Count != want.Count {
|
|
||||||
t.Errorf("TopPrefixes()[%d] = {Prefix: %q, Count: %d}, want {Prefix: %q, Count: %d}",
|
|
||||||
i, got[i].Prefix, got[i].Count, want.Prefix, want.Count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTopPrefixesSorting(t *testing.T) {
|
|
||||||
root := New()
|
|
||||||
words := []string{"a", "ab", "abc", "abcd", "abcde"}
|
|
||||||
for _, word := range words {
|
|
||||||
root.Add(word)
|
|
||||||
}
|
|
||||||
|
|
||||||
prefixes := TopPrefixes(root)
|
|
||||||
|
|
||||||
// Verify sorting by count (descending)
|
|
||||||
for i := 1; i < len(prefixes); i++ {
|
|
||||||
if prefixes[i].Count > prefixes[i-1].Count {
|
|
||||||
t.Errorf("Prefixes not sorted by count: %v", prefixes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify alphabetical sorting for equal counts
|
|
||||||
for i := 1; i < len(prefixes); i++ {
|
|
||||||
if prefixes[i].Count == prefixes[i-1].Count && prefixes[i].Prefix < prefixes[i-1].Prefix {
|
|
||||||
t.Errorf("Prefixes with equal counts not sorted alphabetically: %v", prefixes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -3,11 +3,9 @@ package database
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"git.sdf.org/jchenry/x"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Func func(ctx context.Context, db *sql.DB) error
|
type Func func(db *sql.DB)
|
||||||
|
|
||||||
type Actor struct {
|
type Actor struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
@@ -15,13 +13,10 @@ type Actor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Actor) Run(ctx context.Context) error {
|
func (a *Actor) Run(ctx context.Context) error {
|
||||||
x.Assert(ctx != nil, "Actor.Run: context cannot be nil")
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case f := <-a.ActionChan:
|
case f := <-a.ActionChan:
|
||||||
if err:= f(ctx, a.DB); err != nil{
|
f(a.DB)
|
||||||
return err
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
@@ -7,21 +7,9 @@ Example usage:
|
|||||||
ctx, _ := context.WithCancel(context.Background())
|
ctx, _ := context.WithCancel(context.Background())
|
||||||
dba = &db.Actor{
|
dba = &db.Actor{
|
||||||
DB: s.db,
|
DB: s.db,
|
||||||
ActionChan: make(chan database.Func),
|
ActionChan: make(chan db.Func),
|
||||||
}
|
}
|
||||||
|
|
||||||
go dba.Run(ctx)
|
go dba.Run(ctx)
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
|
||||||
Transactor Example:
|
|
||||||
|
|
||||||
func insert(ctx context.Context, db *sql.DB) error{
|
|
||||||
// SQL HERE
|
|
||||||
}
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
dba.ActionChan <- db.WithTransaction(insert)
|
|
||||||
*/
|
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WithinTransaction is a functional equivalent of the Transactor interface created by Thibaut Rousseau's
|
|
||||||
// https://blog.thibaut-rousseau.com/blog/sql-transactions-in-go-the-good-way/
|
|
||||||
func WithinTransaction(f Func) Func {
|
|
||||||
return func(ctx context.Context, db *sql.DB) error {
|
|
||||||
tx, err := db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to begin transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f(ctx, db); err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return fmt.Errorf("failed to commit transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
4
go.mod
4
go.mod
@@ -1,3 +1,3 @@
|
|||||||
module git.sdf.org/jchenry/x
|
module github.com/jchenry/x
|
||||||
|
|
||||||
go 1.22
|
go 1.18
|
||||||
|
15
log/logger.go
Normal file
15
log/logger.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
// Logger is a logging interface with only the essentials that a function that needs to log should care about. Compatible with standard Go logger.
|
||||||
|
type Logger interface {
|
||||||
|
Print(v ...interface{})
|
||||||
|
Printf(format string, v ...interface{})
|
||||||
|
Println(v ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// None provides a logger that doesnt log anything
|
||||||
|
type None struct{}
|
||||||
|
|
||||||
|
func (n None) Print(v ...interface{}) {}
|
||||||
|
func (n None) Printf(format string, v ...interface{}) {}
|
||||||
|
func (n None) Println(v ...interface{}) {}
|
@@ -6,13 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.sdf.org/jchenry/x"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func BasicAuth(h http.Handler, htpasswd map[string]string, realm string) http.HandlerFunc {
|
func BasicAuth(h http.Handler, htpasswd map[string]string, realm string) http.HandlerFunc {
|
||||||
x.Assert(len(htpasswd) > 0, "http.BasicAuth: htpassword cannot be empty")
|
|
||||||
x.Assert(len(realm) > 0, "http.BasicAuth: realm cannot be empty")
|
|
||||||
rlm := fmt.Sprintf(`Basic realm="%s"`, realm)
|
rlm := fmt.Sprintf(`Basic realm="%s"`, realm)
|
||||||
sha1 := func(password string) string {
|
sha1 := func(password string) string {
|
||||||
s := sha1.New()
|
s := sha1.New()
|
||||||
@@ -24,7 +20,7 @@ func BasicAuth(h http.Handler, htpasswd map[string]string, realm string) http.Ha
|
|||||||
user, pass, _ := r.BasicAuth()
|
user, pass, _ := r.BasicAuth()
|
||||||
if pw, ok := htpasswd[user]; !ok || !strings.EqualFold(pass, sha1(pw)) {
|
if pw, ok := htpasswd[user]; !ok || !strings.EqualFold(pass, sha1(pw)) {
|
||||||
w.Header().Set("WWW-Authenticate", rlm)
|
w.Header().Set("WWW-Authenticate", rlm)
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
http.Error(w, "Unauthorized", 401)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
|
@@ -30,7 +30,7 @@ func MutliHandler(h map[string]http.Handler) (http.HandlerFunc, error) {
|
|||||||
if hdlr, ok := h[r.Method]; ok {
|
if hdlr, ok := h[r.Method]; ok {
|
||||||
hdlr.ServeHTTP(w, r)
|
hdlr.ServeHTTP(w, r)
|
||||||
} else {
|
} else {
|
||||||
NotAllowedHandler.ServeHTTP(w, r)
|
NotImplementedHandler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
95
net/http/mux.go
Normal file
95
net/http/mux.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServeMux struct {
|
||||||
|
routes []route
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *ServeMux) Handle(pattern string, handler http.Handler, pathParams ...any) {
|
||||||
|
mux.routes = append(mux.routes, newRoute(pattern, handler, pathParams...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *ServeMux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request), pathParams ...any) {
|
||||||
|
mux.routes = append(mux.routes, newRoute(pattern, http.HandlerFunc(handler), pathParams...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *ServeMux) Handler(r *http.Request) (h http.Handler, pattern string) {
|
||||||
|
for _, rte := range mux.routes {
|
||||||
|
switch {
|
||||||
|
case rte.matcher(r):
|
||||||
|
return rte.handler, rte.pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http.HandlerFunc(http.NotFound), ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.RequestURI == "*" {
|
||||||
|
if r.ProtoAtLeast(1, 1) {
|
||||||
|
w.Header().Set("Connection", "close")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h, _ := mux.Handler(r)
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type route struct {
|
||||||
|
pattern string
|
||||||
|
matcher func(r *http.Request) bool
|
||||||
|
handler http.HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRoute(pattern string, handler http.Handler, vars ...interface{}) route {
|
||||||
|
return route{
|
||||||
|
pattern,
|
||||||
|
func(r *http.Request) bool {
|
||||||
|
return match(r.URL.Path, pattern, vars...)
|
||||||
|
},
|
||||||
|
handler.ServeHTTP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match reports whether path matches the given pattern, which is a
|
||||||
|
// path with '+' wildcards wherever you want to use a parameter. Path
|
||||||
|
// parameters are assigned to the pointers in vars (len(vars) must be
|
||||||
|
// the number of wildcards), which must be of type *string or *int.
|
||||||
|
func match(path, pattern string, vars ...interface{}) bool {
|
||||||
|
for ; pattern != "" && path != ""; pattern = pattern[1:] {
|
||||||
|
switch pattern[0] {
|
||||||
|
case '+':
|
||||||
|
// '+' matches till next slash in path
|
||||||
|
slash := strings.IndexByte(path, '/')
|
||||||
|
if slash < 0 {
|
||||||
|
slash = len(path)
|
||||||
|
}
|
||||||
|
segment := path[:slash]
|
||||||
|
path = path[slash:]
|
||||||
|
switch p := vars[0].(type) {
|
||||||
|
case *string:
|
||||||
|
*p = segment
|
||||||
|
case *int:
|
||||||
|
n, err := strconv.Atoi(segment)
|
||||||
|
if err != nil || n < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*p = n
|
||||||
|
default:
|
||||||
|
panic("vars must be *string or *int")
|
||||||
|
}
|
||||||
|
vars = vars[1:]
|
||||||
|
case path[0]:
|
||||||
|
// non-'+' pattern byte must match path byte
|
||||||
|
path = path[1:]
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path == "" && pattern == ""
|
||||||
|
}
|
@@ -15,8 +15,7 @@ func (s StatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
NotFoundHandler = StatusHandler(http.StatusNotFound)
|
NotFoundHandler = StatusHandler(404)
|
||||||
NotImplementedHandler = StatusHandler(http.StatusNotImplemented)
|
NotImplementedHandler = StatusHandler(501)
|
||||||
NotLegalHandler = StatusHandler(http.StatusUnavailableForLegalReasons)
|
NotLegalHandler = StatusHandler(451)
|
||||||
NotAllowedHandler = StatusHandler(http.StatusMethodNotAllowed)
|
|
||||||
)
|
)
|
||||||
|
96
pkg.go
96
pkg.go
@@ -1,96 +0,0 @@
|
|||||||
package x
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Assert evalueates a condition and if it fails, panics.
|
|
||||||
func Assert(cond bool, msg string) {
|
|
||||||
if !cond {
|
|
||||||
panic(errors.New(msg))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check evaluates a condition, adds an error to its list and continues
|
|
||||||
func Check(cond bool, err error) I {
|
|
||||||
return new(invariants).Check(cond, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// func ExampleCheck() {
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
type I interface {
|
|
||||||
Check(cond bool, err error) I
|
|
||||||
Join() error
|
|
||||||
First() error
|
|
||||||
All() []error
|
|
||||||
}
|
|
||||||
type invariants struct {
|
|
||||||
errs []error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check evaluates a condition, adds an error to its list and continues
|
|
||||||
func (i *invariants) Check(cond bool, err error) I {
|
|
||||||
if !cond {
|
|
||||||
i.errs = append(i.errs, err)
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// Join returns all an error wrapping all errors that have been seen by the checks
|
|
||||||
func (i *invariants) Join() error {
|
|
||||||
return errors.Join(i.errs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First returns the first error found by the checks
|
|
||||||
func (i *invariants) First() error {
|
|
||||||
if len(i.errs) > 0 {
|
|
||||||
return i.errs[0]
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// All returns all errors found by the checks as a list of errors
|
|
||||||
func (i *invariants) All() []error {
|
|
||||||
return i.errs
|
|
||||||
}
|
|
||||||
|
|
||||||
type xError struct {
|
|
||||||
LineNo int
|
|
||||||
File string
|
|
||||||
E error
|
|
||||||
Debug bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *xError) Error() string {
|
|
||||||
if e.Debug {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"%s\n\t%s:%d", e.E, e.File, e.LineNo,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return e.E.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *xError) Unwrap() error {
|
|
||||||
return e.E
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewError return an error that wrapped an error and optionally provides the file and line number the error occured on
|
|
||||||
func NewError(unwrapped error, debug bool) error {
|
|
||||||
_, file, line, ok := runtime.Caller(1)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
file = "unknown"
|
|
||||||
line = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return &xError{
|
|
||||||
LineNo: line,
|
|
||||||
File: file,
|
|
||||||
E: unwrapped,
|
|
||||||
Debug: debug,
|
|
||||||
}
|
|
||||||
}
|
|
95
rest/collection.go
Executable file
95
rest/collection.go
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jchenry/x/encoding"
|
||||||
|
"github.com/jchenry/x/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Example: Resource(p, c, JSONEncoder, json.Decode(func()interface{}{return &foo{}}), log.None{})
|
||||||
|
func Resource(p *sync.Pool, g Gateway, e EntityEncoder, d encoding.Decoder, l log.Logger) http.HandlerFunc {
|
||||||
|
return restVerbHandler(
|
||||||
|
GetResource(g, e, l),
|
||||||
|
PostResource(g, d, p, l),
|
||||||
|
PutResource(g, e, d, p, l),
|
||||||
|
DeleteResource(g, l),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetResource(store Readable, encode EntityEncoder, log log.Logger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) { // GET
|
||||||
|
if id := filepath.Base(r.URL.Path); id != "" {
|
||||||
|
if e, err := store.Read(id); err == nil { // handle individual entity
|
||||||
|
encode(w, e)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Printf("Error: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if params, err := url.ParseQuery(r.URL.RawQuery); err == nil {
|
||||||
|
if e, err := store.All(params); err == nil { // handle all entities
|
||||||
|
encode(w, e)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Printf("Error: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PostResource(store Creatable, decode encoding.Decoder, pool *sync.Pool, log log.Logger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) { // POST TODO
|
||||||
|
e := pool.Get()
|
||||||
|
defer pool.Put(e)
|
||||||
|
if err := decode(r.Body, e); err == nil {
|
||||||
|
if err = store.Create(e); err == nil {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Printf("Error: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PutResource(store Updatable, encode EntityEncoder, decode encoding.Decoder, pool *sync.Pool, log log.Logger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) { // PUT TODO
|
||||||
|
e := pool.Get()
|
||||||
|
defer pool.Put(e)
|
||||||
|
if err := decode(r.Body, e); err == nil {
|
||||||
|
if err = store.Update(e); err == nil {
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
|
encode(w, e)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Printf("Error: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteResource(store Deletable, log log.Logger) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) { // DELETE TODO
|
||||||
|
if id := filepath.Base(r.URL.Path); id != "" {
|
||||||
|
if err := store.Delete(id); err == nil {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
log.Printf("Error: %s", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
rest/entity_handler.go
Normal file
23
rest/entity_handler.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
gohttp "net/http"
|
||||||
|
|
||||||
|
"github.com/jchenry/x/net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EntityHandler returns a handler that provides restful verbs, following a CRUD model
|
||||||
|
func restVerbHandler(
|
||||||
|
get gohttp.Handler,
|
||||||
|
post gohttp.Handler,
|
||||||
|
put gohttp.Handler,
|
||||||
|
delete gohttp.Handler,
|
||||||
|
) gohttp.HandlerFunc {
|
||||||
|
h, _ := http.MutliHandler(map[string]gohttp.Handler{
|
||||||
|
gohttp.MethodGet: get,
|
||||||
|
gohttp.MethodPost: post,
|
||||||
|
gohttp.MethodPut: put,
|
||||||
|
gohttp.MethodDelete: delete,
|
||||||
|
})
|
||||||
|
return h
|
||||||
|
}
|
25
rest/gateway.go
Normal file
25
rest/gateway.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
type Creatable interface {
|
||||||
|
Create(e interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Updatable interface {
|
||||||
|
Update(e interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Deletable interface {
|
||||||
|
Delete(id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Readable interface {
|
||||||
|
All(filters map[string][]string) (interface{}, error)
|
||||||
|
Read(id string) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Gateway interface {
|
||||||
|
Creatable
|
||||||
|
Updatable
|
||||||
|
Deletable
|
||||||
|
Readable
|
||||||
|
}
|
32
rest/response_encoder.go
Normal file
32
rest/response_encoder.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/jchenry/x/encoding"
|
||||||
|
"github.com/jchenry/x/encoding/json"
|
||||||
|
"github.com/jchenry/x/encoding/xml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntityEncoder func(w http.ResponseWriter, e interface{})
|
||||||
|
|
||||||
|
func JSONEncoder(w http.ResponseWriter, e interface{}) error {
|
||||||
|
return EntityResponseEncoder(w, "application/json", json.Encoder, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func XMLEncoder(w http.ResponseWriter, e interface{}) error {
|
||||||
|
return EntityResponseEncoder(w, "application/xml", xml.Encoder, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func EntityResponseEncoder(w http.ResponseWriter, contentType string, encoder encoding.Encoder, e interface{}) error {
|
||||||
|
w.Header().Set("content-type", contentType)
|
||||||
|
return encoder(w, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorResponseEncoder(w http.ResponseWriter, contentType string, encoder encoding.Encoder, status int, err error) error {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
return EntityResponseEncoder(w, contentType, encoder, map[string]interface{}{
|
||||||
|
"status": status,
|
||||||
|
"message": err.Error,
|
||||||
|
})
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
package snowflake
|
package snowflake
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
@@ -14,48 +15,42 @@ const (
|
|||||||
nodeIDBits = 10
|
nodeIDBits = 10
|
||||||
sequenceBits = 12
|
sequenceBits = 12
|
||||||
|
|
||||||
// Custom Epoch (January 1, 2015 Midnight UTC = 2015-01-01T00:00:00Z) .
|
// Custom Epoch (January 1, 2015 Midnight UTC = 2015-01-01T00:00:00Z)
|
||||||
customEpoch int64 = 1420070400000
|
customEpoch int64 = 1420070400000
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var maxNodeID int64
|
||||||
maxNodeID int64
|
var maxSequence int64
|
||||||
maxSequence int64
|
|
||||||
timestampMutex sync.Mutex
|
|
||||||
sequenceMutex sync.Mutex
|
|
||||||
nodeID int64
|
|
||||||
lastTimestamp int64 = 0
|
|
||||||
sequence int64
|
|
||||||
)
|
|
||||||
|
|
||||||
const two = 2
|
var nodeID int64
|
||||||
|
var lastTimestamp int64 = 0
|
||||||
|
var sequence int64
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
maxNodeID = int64(math.Pow(two, nodeIDBits) - 1)
|
maxNodeID = int64(math.Pow(2, nodeIDBits) - 1)
|
||||||
maxSequence = int64(math.Pow(two, sequenceBits) - 1)
|
maxSequence = int64(math.Pow(2, sequenceBits) - 1)
|
||||||
nodeID = generateNodeID()
|
nodeID = generateNodeID()
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateNodeID() int64 {
|
func generateNodeID() int64 {
|
||||||
var nodeID int64
|
var nodeID int64
|
||||||
|
|
||||||
if interfaces, err := net.Interfaces(); err == nil {
|
if interfaces, err := net.Interfaces(); err == nil {
|
||||||
h := fnv.New32a()
|
h := fnv.New32a()
|
||||||
for _, i := range interfaces {
|
for _, i := range interfaces {
|
||||||
h.Write(i.HardwareAddr)
|
h.Write(i.HardwareAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeID = int64(h.Sum32())
|
nodeID = int64(h.Sum32())
|
||||||
} else {
|
} else {
|
||||||
panic("interfaces not available")
|
panic("interfaces not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeID = nodeID & maxNodeID
|
nodeID = nodeID & maxNodeID
|
||||||
|
|
||||||
return nodeID
|
return nodeID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next returns the next logical snowflake.
|
var timestampMutex sync.Mutex
|
||||||
|
var sequenceMutex sync.Mutex
|
||||||
|
|
||||||
|
// Next returns the next logical snowflake
|
||||||
func Next() int64 {
|
func Next() int64 {
|
||||||
timestampMutex.Lock()
|
timestampMutex.Lock()
|
||||||
currentTimestamp := ts()
|
currentTimestamp := ts()
|
||||||
@@ -81,6 +76,7 @@ func Next() int64 {
|
|||||||
id |= (nodeID << sequenceBits)
|
id |= (nodeID << sequenceBits)
|
||||||
id |= sequence
|
id |= sequence
|
||||||
|
|
||||||
|
fmt.Printf("%b\n", id)
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +88,5 @@ func waitNextMillis(currentTimestamp int64) int64 {
|
|||||||
for currentTimestamp == lastTimestamp {
|
for currentTimestamp == lastTimestamp {
|
||||||
currentTimestamp = ts()
|
currentTimestamp = ts()
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentTimestamp
|
return currentTimestamp
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ func TestNext(t *testing.T) {
|
|||||||
fmt.Printf("node id: %b\n", generateNodeID())
|
fmt.Printf("node id: %b\n", generateNodeID())
|
||||||
fmt.Printf("timestamp: %b\n", ts())
|
fmt.Printf("timestamp: %b\n", ts())
|
||||||
fmt.Printf("full token: %b\n", Next())
|
fmt.Printf("full token: %b\n", Next())
|
||||||
|
// t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkNext(b *testing.B) {
|
func BenchmarkNext(b *testing.B) {
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
type Arvelie string
|
type Arvelie string
|
||||||
|
|
||||||
func (a *Arvelie) IsValid() bool {
|
func (a *Arvelie) isValid() bool {
|
||||||
if a != nil {
|
if a != nil {
|
||||||
return strings.EqualFold(string(*a), string(FromDate(ToDate(*a))))
|
return strings.EqualFold(string(*a), string(FromDate(ToDate(*a))))
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ func ToDate(a Arvelie) time.Time {
|
|||||||
mon = (int(m[0]) - 65)
|
mon = (int(m[0]) - 65)
|
||||||
}
|
}
|
||||||
|
|
||||||
doty := (math.Floor(float64(mon)*14) + float64(d) - 1)
|
doty := (math.Floor(float64(mon)*14) + math.Floor(float64(d)) - 1)
|
||||||
yr, _ := strconv.Atoi(fmt.Sprintf("20%s", y))
|
yr, _ := strconv.Atoi(fmt.Sprintf("20%s", y))
|
||||||
return time.Date(yr, 1, 1, 0, 0, 0, 0, time.UTC).AddDate(0, 0, int(doty))
|
return time.Date(yr, 1, 1, 0, 0, 0, 0, time.UTC).AddDate(0, 0, int(doty))
|
||||||
}
|
}
|
||||||
@@ -42,15 +42,17 @@ func FromDate(date time.Time) Arvelie {
|
|||||||
if doty == 365 || doty == 366 {
|
if doty == 365 || doty == 366 {
|
||||||
m = "+"
|
m = "+"
|
||||||
} else {
|
} else {
|
||||||
m = strings.ToUpper(string([]byte{byte(97 + math.Floor(float64(doty)/14))}))
|
m = strings.ToUpper(string([]byte{byte(97 + math.Floor(float64(doty/14)))}))
|
||||||
}
|
}
|
||||||
|
|
||||||
var d string
|
var d string
|
||||||
switch doty {
|
switch doty {
|
||||||
case 365:
|
case 365:
|
||||||
d = fmt.Sprintf("%02d", 1)
|
d = fmt.Sprintf("%02d", 1)
|
||||||
|
break
|
||||||
case 366:
|
case 366:
|
||||||
d = fmt.Sprintf("%02d", 2)
|
d = fmt.Sprintf("%02d", 2)
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
d = fmt.Sprintf("%02d", (doty % 14))
|
d = fmt.Sprintf("%02d", (doty % 14))
|
||||||
}
|
}
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sdf.org/jchenry/x/time/arvelie"
|
"github.com/jchenry/x/time/arvelie"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFromDate(t *testing.T) {
|
func TestFromDate(t *testing.T) {
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
package time
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const ISO8601 = time.RFC3339
|
|
@@ -6,7 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.sdf.org/jchenry/x/time/neralie"
|
"github.com/jchenry/x/time/neralie"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFromTime(t *testing.T) {
|
func TestFromTime(t *testing.T) {
|
||||||
|
Reference in New Issue
Block a user