Files
Fredrik Foss-Indrehus 705a3659f0 lib/jwz: use slices.SortFunc for thread sorting
Replace sort.Slice with slices.SortFunc and adapt ThreadLess to the
three-way comparator form.

Signed-off-by: Fredrik Foss-Indrehus <fredrik@ffoss.net>
Acked-by: Robin Jarry <robin@jarry.cc>
2026-07-18 15:23:18 +02:00

204 lines
7.3 KiB
Go

// Author: Jim Idle - jimi@idle.ws / jimi@gatherstars.com
// SPDX-License-Identifier: Apache-2.0
package jwz
import "slices"
// ThreadLess specifies the signature of a function that compares two Threadables in some way you define,
// such as comparing dates in the emails they contain. Note your function should be able to handle Dummy
// Threadables in some sensible way, such as using the child of the Dummy for the sort parameters.
//
// ThreadLess reports whether the Threadable t1 must sort before the Threadable t2.
//
// If both ThreadLess(t1, t2) and ThreadLess(t2, t1) are false, then t1 and t2 are considered equal.
// Sort may place equal elements in any order in the final result.
//
// ThreadLess must describe a transitive ordering:
//
// - if both ThreadLess(t1, t2) and ThreadLess(t2, t3) are true, then ThreadLess(t1, t3) must be true as well.
// - if both ThreadLess(t1, t2) and ThreadLess(t2, t3) are false, then ThreadLess(t1, t3) must be false as well.
type ThreadLess func(t1 Threadable, t2 Threadable) bool
// WalkFunction specifies the signature of a function that can be called by the generic Walk utility function.
// As well as being passed the current Threadable in the walk, the function will be passed an interface of your
// choosing, which is passed in to the walk function and then propagated to each call of this function. T
//
// The Walk will not interact with the any, just keep it accessible to your walk function, hence you
// can pass nil if you do not need it.
//
// If your searcher function returns true, then the tree walk will end where it is
type WalkFunction func(t Threadable, u any) (bool, error)
// Count will traverse the supplied Threadable and store the count of Threadables contained within it in the
// given counter location.
//
// Note that any Dummy placeholder nodes are excluded from the count.
func Count(root Threadable, counter *int) {
if root == nil {
return
}
for node := root; node != nil; node = node.GetNext() {
if c := node.GetChild(); c != nil {
// Count children of the current one first then
//
Count(c, counter)
}
// Only count this one if it is not a dummy placeholder
//
if !node.IsDummy() {
*counter++
}
}
}
// Sort will create order from the chaos created by threading a set of emails, that even if given as input
// in a specific order, will be threaded in whatever order the go data structures happen to spit out - which
// will usually be a different order each time.
//
// Note that Sort will not change the embedded nature of the Threads. As in, while the child of a particular
// Threadable can, and usually will, be changed, the new child will belong to the set of next Threadables that
// its original child pointer belonged to. If sorting by date for instance, the set of reply threads/emails
// will be ordered such that the replies are in date order, which is what you want. However, the set of replies
// will remain the same, as the song goes.
//
// Note that you should use the returned Threadable as the new root of your threads, as your old one is
// very likely to have been moved halfway down the top level chain that you passed in. That will confuse the
// BeJesus out of you. I know it did me for a minute.
//
// So, all the chains will be sorted by asking your supplied ThreadLess function whether the first Threadable
// it gives you should sort before the second one it gives you. This makes the sort trivial for you to sort
// the Threadable set and avoids you having to get your head around following the chain of pointers etc.
func Sort(threads Threadable, by ThreadLess) Threadable {
// Guard against stupidity
//
if threads == nil {
return nil
}
// If this node has no next pointers, then it is the only element in this current chain, so it is
// sorted by default
//
if threads.GetNext() == nil {
return threads
}
// Now we sort the chain of current pointers. The easiest way is to convert to a slice then
// sort the slice. This is because after sorting, the child of the node above should become the
// first element of the sorted slice
//
s := make([]Threadable, 0, 42)
for current := threads; current != nil; current = current.GetNext() {
// If the current node we are inspecting has a child, then sort that first
//
if c := current.GetChild(); c != nil {
current.SetChild(Sort(c, by))
}
s = append(s, current)
}
// We can now sort the slice of next pointers at this level. Note that the child of this node
// will already have been sorted, so if it is used to get the date or something like that
// - because this node is a Dummy - then it will be OK to use it as it will be in the correct
// order already
//
slices.SortFunc(s, func(a, b Threadable) int {
if by(a, b) {
return -1
}
if by(b, a) {
return 1
}
return 0
})
// And we now rebuild the chain from the slice
//
l := len(s) - 1
for i := range l {
s[i].SetNext(s[i+1])
}
// Last element in the slice no longer has a current of course
//
s[l].SetNext(nil)
// And the new child of the node above is the first of the newly sorted (or at the top level the new root)
//
newChild := s[0]
// And return the new child for the node above us
//
return newChild
}
// Walk allows the caller to execute some function against each node in the tree, while avoiding dealing with the
// internal structure of the Threadable tree. It is easy to get tree walk code wrong, and while this generic walk
// will probably not do everything that everyone wants - such as cause Leeds United to beat Manchester United - it
// is likely to work for most cases.
//
// Walk will call your WalkFunction for each node in the tree and will supply the node, and an interface value
// (which can be nil) that you can supply for your own tracking if simple walk is not enough for you.
//
// The walker will perform depth first search if asked, by passing parameter isDepth=true
func Walk(isDepth bool, tree Threadable, f WalkFunction, u any) error {
// Guard against stupidity
//
if tree == nil {
return nil
}
// A depth first search means we descend in to the children until there aren't any, then traverse the
// next pointers. A breadth first search means we traverse all the next pointers then descend in to the children
//
if isDepth {
for current := tree; current != nil; current = current.GetNext() {
// If the current node we are inspecting has a child, then call with that first
//
if c := current.GetChild(); c != nil {
if err := Walk(isDepth, c, f, u); err != nil {
return err
}
}
// Now call the user function on this current node, which of course means we eventually call them all
//
stop, err := f(current, u)
if err != nil || stop {
return err
}
}
} else {
// This is breadth first, which means we call the function with all the Threadables in the next
// chain, then we call their children if they have any
//
for current := tree; current != nil; current = current.GetNext() {
// Call the function on each next in turn
//
stop, err := f(current, u)
if err != nil || stop {
return err
}
}
for current := tree; current != nil; current = current.GetNext() {
// If the current node we are inspecting has a child, then walk it breadth wise
//
if c := current.GetChild(); c != nil {
if err := Walk(isDepth, c, f, u); err != nil {
return err
}
}
}
}
// We are finished with this bit
//
return nil
}