6 Commits

Author SHA1 Message Date
Colin Henry
3d71b65428 added trie
Some checks failed
Go / build (1.23) (push) Failing after 2m42s
2025-07-05 11:56:37 -07:00
Colin Henry
4cf0b874d6 fixed tio arvelie 2025-07-05 11:56:37 -07:00
Colin Henry
475e091d21 remove drone.yml
All checks were successful
Go / build (1.23) (push) Successful in 2m53s
2025-06-20 00:07:25 -07:00
Colin Henry
56752521c2 added doc
All checks were successful
Go / build (1.23) (push) Successful in 2m31s
2025-03-18 22:28:20 -07:00
Colin Henry
6aa7788fd0 added transactional wrapper, and support for it 2025-03-10 22:59:03 -07:00
Colin Henry
f88f00e78c fixed release target
All checks were successful
Go / build (1.23) (push) Successful in 17s
2024-10-27 12:30:45 -07:00
8 changed files with 314 additions and 19 deletions

View File

@@ -1,10 +0,0 @@
kind: pipeline
type: docker
name: default
steps:
- name: test
image: golang
commands:
- go test ./...
- go build ./...

View File

@@ -13,7 +13,7 @@ endef
# Target to tag and push the next version
release:
@next_version=$$($(next_version)); \
echo "Tagging with version $$next_version";
next_version=$$($(next_version)); \
echo "Tagging with version $$next_version"; \
git tag $$next_version; \
git push $(REPO_REMOTE) $$next_version

101
container/trie/trie.go Normal file
View 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
View 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)
}
}
}

View File

@@ -7,7 +7,7 @@ import (
"git.sdf.org/jchenry/x"
)
type Func func(db *sql.DB)
type Func func(ctx context.Context, db *sql.DB) error
type Actor struct {
DB *sql.DB
@@ -19,7 +19,9 @@ func (a *Actor) Run(ctx context.Context) error {
for {
select {
case f := <-a.ActionChan:
f(a.DB)
if err:= f(ctx, a.DB); err != nil{
return err
}
case <-ctx.Done():
return ctx.Err()
}

View File

@@ -13,3 +13,15 @@ Example usage:
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
View 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
}
}

View File

@@ -10,7 +10,7 @@ import (
type Arvelie string
func (a *Arvelie) isValid() bool {
func (a *Arvelie) IsValid() bool {
if a != nil {
return strings.EqualFold(string(*a), string(FromDate(ToDate(*a))))
}
@@ -29,7 +29,7 @@ func ToDate(a Arvelie) time.Time {
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))
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 {
m = "+"
} 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
switch doty {
case 365:
d = fmt.Sprintf("%02d", 1)
break
case 366:
d = fmt.Sprintf("%02d", 2)
break
default:
d = fmt.Sprintf("%02d", (doty % 14))
}