Provided build in Cabal, scaffolded project.

This commit is contained in:
2026-07-14 19:37:49 -05:00
parent f3cd31abf6
commit ef61240fb8
4 changed files with 212 additions and 1 deletions

View File

@@ -1,3 +1,13 @@
# h2h_sorter # h2h_sorter
Efficient head-to-head sorter with backtracking in Haskell. Purpose-made to rank anime girl casts, but will work on any plaintext source. Efficient head-to-head sorter terminal toy, with backtracking in Haskell.
Purpose-made to rank anime girl casts, but will work on any plaintext source.
## Building and testing
```bash
cabal build
# sort the Madoka cast
cabal run h2h_sorter -- meguca.txt
```

18
h2h-sorter.cabal Normal file
View File

@@ -0,0 +1,18 @@
cabal-version: 3.0
name: h2h-sorter
version: 0.2.0
synopsis: simple head-to-head sorter with backtracking.
license: GPL-3.0-or-later
license-file: LICENSE
author: hpcdisrespecter
build-type: Simple
extra-doc-files: README.md
executable h2h_sorter
main-is: Main.hs
hs-source-dirs: src
build-depends: base >=4.14 && <5
, array
, mtl
, random
default-language: Haskell2010
ghc-options: -Wall -O2

5
meguca.txt Normal file
View File

@@ -0,0 +1,5 @@
pink
black
yellow
red
blue

178
src/Main.hs Normal file
View File

@@ -0,0 +1,178 @@
module Main where
import Control.Monad (when, forM_)
import Control.Monad.State (StateT, execStateT, get, put, modify, lift)
import Data.Array.IO
import Data.List (find, sortBy)
import System.Environment (getArgs)
import System.IO (hSetEcho, hSetBuffering, stdin, stdout, BufferMode(NoBuffering), hFlush)
import System.Random (randomRIO)
import System.Exit (exitSuccess)
-- Types of user choices
data Verdict = GoLeft | GoRight deriving (Eq, Show)
data Preference = Choice Verdict | GoBack | QuitNow deriving (Eq, Show)
data Comparison = Comp String String Verdict deriving (Show)
-- The application state
data AppState = AppState
{ items :: [String]
, history :: [Comparison]
, original :: [String]
} deriving (Show)
-- Initialize the state
initialState :: [String] -> AppState
initialState xs = AppState { items = xs, history = [] , original = xs }
-- Tails gets trolled by the type system
tails :: [a] -> [[a]]
tails [] = [[]]
tails xs@(_:xs') = xs : tails xs'
-- helper function for arrays
newArrayFrom :: [a] -> IO (IOArray Int a)
newArrayFrom xs = newListArray (0, length xs - 1) xs
-- O(n) Fisher-Yates shuffle
shuffle :: [a] -> IO [a]
shuffle xs = do
let n = length xs
if n < 2
then return xs
else do
arr <- newArrayFrom xs
forM_ [n - 1, n - 2 .. 1] $ \i -> do
j <- randomRIO (0, i)
vi <- readArray arr i
vj <- readArray arr j
writeArray arr j vi
writeArray arr i vj
getElems arr
-- Read a single key, handling arrow keys
getKey :: IO Preference
getKey = do
hSetEcho stdin False
c <- getChar
hSetEcho stdin True
case c of
'\ESC' -> do
_c2 <- getChar
c3 <- getChar
case c3 of
'D' -> putStrLn "" >> return (Choice GoLeft)
'C' -> putStrLn "" >> return (Choice GoRight)
_ -> getKey
'b' -> putStrLn "b" >> return GoBack
'q' -> putStrLn "q" >> return QuitNow
'h' -> putStrLn "" >> return (Choice GoLeft)
'l' -> putStrLn "" >> return (Choice GoRight)
'k' -> putStrLn "b" >> return GoBack
_ -> getKey
-- The shared comparison function that tracks history
compareItems :: String -> String -> StateT AppState IO Verdict
compareItems x y = do
lift $ putStr $ x ++ " vs. " ++ y ++ " (←/→/b/q): "
lift $ hFlush stdout
choice <- lift getKey
case choice of
Choice GoLeft -> do
modify (\s -> s { history = Comp x y GoLeft : history s })
return GoLeft
Choice GoRight -> do
modify (\s -> s { history = Comp x y GoRight : history s })
return GoRight
GoBack -> handleBack
QuitNow -> lift $ exitSuccess
-- Handle going back
handleBack :: StateT AppState IO Verdict
handleBack = do
s <- get
case history s of
(Comp x y _ : rest) -> do
put $ s { history = rest }
compareItems x y
[] -> do
lift $ putStrLn "No more history to go back to. Restarting..."
put $ s { history = [] }
compareItems (items s !! 0) ( items s !! 1 )
-- Check transitive closure
isLessThan :: [Comparison] -> String -> String -> Bool
isLessThan hist x y
| x == y = False
| otherwise = go [x] []
where
go [] _ = False
go (z:zs) visited
| z == y = True
| z `elem` visited = go zs visited
| otherwise = case filter (\(Comp a _ _) -> a == z) hist of
[] -> go zs (z:visited)
comps -> any (\(Comp _ b p) -> p == GoLeft && go (b:zs) (z:visited)) comps
|| go zs (z:visited)
-- Compare function using history
compareWithHistory :: [Comparison] -> String -> String -> Ordering
compareWithHistory hist x y
| x == y = EQ
| isLessThan hist x y = LT
| isLessThan hist y x = GT
| otherwise = case find (\(Comp a b _) -> (a == x && b == y) || (a == y && b == x)) hist of
Just (Comp a _ GoLeft) -> if a == x then LT else GT
Just (Comp a _ GoRight) -> if a == x then GT else LT
Nothing -> EQ
-- Sort by prompting user for missing comparisons
sortPref :: [String] -> StateT AppState IO [String]
sortPref xs = do
s <- get
let hist = history s
allPairs = [(x, y) | (x:ys) <- tails xs, y <- ys]
missingPairs = filter (\(x, y) -> not (isLessThan hist x y || isLessThan hist y x)) allPairs
mapM_ (\(x, y) -> compareItems x y >> return ()) missingPairs
s' <- get
return $ sortBy (compareWithHistory (history s')) xs
-- Display the sorted results
displayResult :: [String] -> IO ()
displayResult xs = do
putStrLn "\nSorted Result:"
mapM_ (\(i, item) -> putStrLn $ show i ++ ". " ++ item) $ zip [1 :: Int ..] xs
-- Ask user if they want to save the results
askToSave :: [String] -> IO ()
askToSave xs = do
putStr "Would you like to save the sorted list? (y/n) "
hFlush stdout
response <- getChar
putStrLn ""
when (response == 'y' || response == 'Y') $ do
putStr "Enter filename to save: "
hFlush stdout
filename <- getLine
writeFile filename (unlines xs)
putStrLn $ "Sorted list saved to " ++ filename
-- Main function
main :: IO ()
main = do
hSetBuffering stdout NoBuffering
hSetBuffering stdin NoBuffering
args <- getArgs
case args of
[filename] -> do
content <- readFile filename
let ls = lines content
when (null ls) $ do
putStrLn "No items to sort."
exitSuccess
shuffled <- shuffle ls
finalState <- execStateT (sortPref shuffled >>= \sorted -> modify (\s -> s { items = sorted })) (initialState shuffled)
let sortedList = items finalState
displayResult sortedList
askToSave sortedList
_ -> putStrLn "Usage: h2h_sorter [filename]"