2024-03-10 05:37:19 +00:00
|
|
|
// binary manualsort asks you at the CLI which of two things you like
|
|
|
|
// better, repeatedly, and pukes up a list sorted accordingly.
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
2024-03-10 06:23:43 +00:00
|
|
|
"math/rand"
|
2024-03-10 05:37:19 +00:00
|
|
|
"os"
|
|
|
|
"slices"
|
|
|
|
"strings"
|
2024-03-10 06:23:43 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
2024-03-10 05:37:19 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// handwritten merge sort because it minimizes comparisons, which are
|
|
|
|
// the expensive part when asking a human for every comparison.
|
|
|
|
// quicksort's "in-place" behavior isn't necessary.
|
2024-03-10 06:23:43 +00:00
|
|
|
func merge(a, b []string, less func(string, string) bool) []string {
|
2024-03-10 05:37:19 +00:00
|
|
|
if len(a) == 0 {
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
if len(b) == 0 {
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
ret := make([]string, 0, len(a)+len(b))
|
|
|
|
for len(a) > 0 && len(b) > 0 {
|
2024-03-10 06:23:43 +00:00
|
|
|
if less(a[0], b[0]) {
|
2024-03-10 05:37:19 +00:00
|
|
|
ret = append(ret, a[0])
|
|
|
|
a = a[1:]
|
|
|
|
} else {
|
|
|
|
ret = append(ret, b[0])
|
|
|
|
b = b[1:]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(a) > 0 {
|
|
|
|
ret = append(ret, a...)
|
|
|
|
}
|
|
|
|
if len(b) > 0 {
|
|
|
|
ret = append(ret, b...)
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
stdinReader *bufio.Reader
|
|
|
|
truthy = []string{
|
|
|
|
"y", "yes", "1", "t", "true", "aye", "left", "up", "top", "first", "p",
|
|
|
|
}
|
|
|
|
falsey = []string{
|
|
|
|
"n", "no", "0", "2", "f", "false", "nay", "right", "down", "bottom", "second", "nil",
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2024-03-10 06:23:43 +00:00
|
|
|
type Question struct {
|
|
|
|
First string
|
|
|
|
Second string
|
|
|
|
Reply chan<- bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (q *Question) Ask() {
|
|
|
|
q.Reply <- better(q.First, q.Second)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (q *Question) Push(qc chan<- *Question) bool {
|
|
|
|
c := make(chan bool, 1)
|
|
|
|
q.Reply = c
|
|
|
|
qc <- q
|
|
|
|
return <-c
|
|
|
|
}
|
|
|
|
|
|
|
|
func betterLoop(qq <-chan *Question) {
|
|
|
|
var pool []*Question
|
|
|
|
drain := time.After(100 * time.Millisecond)
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case q, ok := <-qq:
|
|
|
|
if !ok {
|
|
|
|
qq = nil
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
pool = append(pool, q)
|
|
|
|
case <-drain:
|
|
|
|
rand.Shuffle(len(pool), func(i, j int) { pool[i], pool[j] = pool[j], pool[i] })
|
|
|
|
for _, q := range pool {
|
|
|
|
q.Ask()
|
|
|
|
}
|
|
|
|
pool = pool[:0]
|
|
|
|
if qq == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
drain = time.After(100 * time.Millisecond)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-10 05:37:19 +00:00
|
|
|
func better(first, second string) bool {
|
|
|
|
for {
|
|
|
|
fmt.Println()
|
|
|
|
fmt.Println("Is")
|
|
|
|
fmt.Println(first)
|
|
|
|
fmt.Println("better than")
|
|
|
|
fmt.Println(second)
|
|
|
|
fmt.Print("? > ")
|
|
|
|
reply, err := stdinReader.ReadString('\n')
|
|
|
|
if err != nil && len(reply) == 0 {
|
|
|
|
log.Fatalf("I/O error: %v", err)
|
|
|
|
}
|
|
|
|
reply = strings.TrimSpace(reply)
|
|
|
|
reply = strings.ToLower(reply)
|
|
|
|
if slices.Contains(truthy, reply) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if slices.Contains(falsey, reply) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
fmt.Printf("Huh? I didn't understand %q. Answer \"y\" or \"n\".\n", reply)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-10 06:23:43 +00:00
|
|
|
func parallelBetter(first, second string, qq chan<- *Question) bool {
|
|
|
|
q := Question{
|
|
|
|
First: first,
|
|
|
|
Second: second,
|
|
|
|
}
|
|
|
|
return q.Push(qq)
|
|
|
|
}
|
|
|
|
|
2024-03-10 05:37:19 +00:00
|
|
|
func mergeSort(items []string) []string {
|
|
|
|
if len(items) <= 1 {
|
|
|
|
return items
|
|
|
|
}
|
|
|
|
midpoint := len(items) / 2
|
2024-03-10 06:23:43 +00:00
|
|
|
return merge(mergeSort(items[:midpoint]), mergeSort(items[midpoint:]), better)
|
|
|
|
}
|
|
|
|
|
|
|
|
func parallelMergeSort(questions chan<- *Question, items []string) []string {
|
|
|
|
if len(items) <= 1 {
|
|
|
|
return items
|
|
|
|
}
|
|
|
|
midpoint := len(items) / 2
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
wg.Add(2)
|
|
|
|
var r1, r2 []string
|
|
|
|
go func() {
|
|
|
|
r1 = parallelMergeSort(questions, items[:midpoint])
|
|
|
|
wg.Done()
|
|
|
|
}()
|
|
|
|
go func() {
|
|
|
|
r2 = parallelMergeSort(questions, (items[midpoint:]))
|
|
|
|
wg.Done()
|
|
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
return merge(r1, r2, func(a, b string) bool {
|
|
|
|
return parallelBetter(a, b, questions)
|
|
|
|
})
|
2024-03-10 05:37:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
if len(os.Args) != 3 {
|
|
|
|
log.Fatal("wrong number of args, expected 2. Usage: manualsort <item list file> <output file path>")
|
|
|
|
}
|
|
|
|
ifile, err := os.Open(os.Args[1])
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("can't open %q: %v.", os.Args[1], err)
|
|
|
|
}
|
|
|
|
ofile, err := os.OpenFile(os.Args[2], os.O_CREATE|os.O_EXCL, 0600)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("can't exclusively create %q: %v.", os.Args[2], err)
|
|
|
|
}
|
|
|
|
stdinReader = bufio.NewReader(os.Stdin)
|
|
|
|
|
|
|
|
var items []string
|
|
|
|
scanner := bufio.NewScanner(ifile)
|
|
|
|
for scanner.Scan() {
|
|
|
|
if t := strings.TrimSpace(scanner.Text()); len(t) > 0 {
|
|
|
|
items = append(items, t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
log.Fatalf("can't read %q: %v", os.Args[1], err)
|
|
|
|
}
|
|
|
|
|
2024-03-10 06:23:43 +00:00
|
|
|
rand.Shuffle(len(items), func(i, j int) {
|
|
|
|
items[i], items[j] = items[j], items[i]
|
|
|
|
})
|
2024-03-10 05:37:19 +00:00
|
|
|
sorted := mergeSort(items)
|
|
|
|
fmt.Println("Sorted. Saving...")
|
|
|
|
obuf := bufio.NewWriter(ofile)
|
|
|
|
for i, s := range sorted {
|
|
|
|
_, err := obuf.WriteString(fmt.Sprintf("%6d %s\n", i+1, s))
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("error saving %d (%s): %v\n", i+1, s, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
obuf.Flush()
|
|
|
|
ofile.Sync()
|
|
|
|
ofile.Close()
|
|
|
|
}
|