// 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" "math/rand" "os" "slices" "strings" "sync" "time" ) // 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. func merge(a, b []string, less func(string, string) bool) []string { 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 { if less(a[0], b[0]) { 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", } ) 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) } } } 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) } } func parallelBetter(first, second string, qq chan<- *Question) bool { q := Question{ First: first, Second: second, } return q.Push(qq) } func mergeSort(items []string) []string { if len(items) <= 1 { return items } midpoint := len(items) / 2 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) }) } func main() { if len(os.Args) != 3 { log.Fatal("wrong number of args, expected 2. Usage: manualsort ") } 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) } rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) qchan := make(chan *Question, len(items)) go betterLoop(qchan) sorted := parallelMergeSort(qchan, items) close(qchan) 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() }