Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3d71b65428 | ||
|
4cf0b874d6 | ||
|
475e091d21 | ||
|
56752521c2 | ||
|
6aa7788fd0 | ||
|
f88f00e78c |
10
.drone.yml
10
.drone.yml
@@ -1,10 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: default
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: test
|
|
||||||
image: golang
|
|
||||||
commands:
|
|
||||||
- go test ./...
|
|
||||||
- go build ./...
|
|
4
Makefile
4
Makefile
@@ -13,7 +13,7 @@ endef
|
|||||||
|
|
||||||
# Target to tag and push the next version
|
# Target to tag and push the next version
|
||||||
release:
|
release:
|
||||||
@next_version=$$($(next_version)); \
|
next_version=$$($(next_version)); \
|
||||||
echo "Tagging with version $$next_version";
|
echo "Tagging with version $$next_version"; \
|
||||||
git tag $$next_version; \
|
git tag $$next_version; \
|
||||||
git push $(REPO_REMOTE) $$next_version
|
git push $(REPO_REMOTE) $$next_version
|
101
container/trie/trie.go
Normal file
101
container/trie/trie.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
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
|
||||||
|
}
|
163
container/trie/trie_test.go
Normal file
163
container/trie/trie_test.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"git.sdf.org/jchenry/x"
|
"git.sdf.org/jchenry/x"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Func func(db *sql.DB)
|
type Func func(ctx context.Context, db *sql.DB) error
|
||||||
|
|
||||||
type Actor struct {
|
type Actor struct {
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
@@ -19,7 +19,9 @@ func (a *Actor) Run(ctx context.Context) error {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case f := <-a.ActionChan:
|
case f := <-a.ActionChan:
|
||||||
f(a.DB)
|
if err:= f(ctx, a.DB); err != nil{
|
||||||
|
return err
|
||||||
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
@@ -13,3 +13,15 @@ Example usage:
|
|||||||
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)
|
||||||
|
*/
|
||||||
|
29
database/transactor.go
Normal file
29
database/transactor.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@@ -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) + math.Floor(float64(d)) - 1)
|
doty := (math.Floor(float64(mon)*14) + 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,17 +42,15 @@ 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))
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user