Align

Posted on 2015-07-27 by nbloomf
Tags: literate-haskell, munge

This post is literate Haskell; you can load the source into GHCi and play along.


module Align where

import Data.List
import System.IO
import System.Environment
import System.Console.GetOpt
import System.Exit

align :: [String] -> String -> String
align [] xs     = xs
align (d:ds) xs = align ds $ align' d xs

align' sep str = unlines $ map stripSpaces $ map concat $ baz
  where
    foo = map (splitBy' sep) $ lines str
    bar = maxLengths foo
    baz = [zipWith (padWith ' ') bar xs | xs <- foo]

    stripSpaces = reverse . dropWhile (==' ') . reverse

splitBy :: (Eq a) => [a] -> [a] -> [[a]]
splitBy sep xs = foo $ filter (isPrefixOf sep . snd) $ zip (inits xs) (tails xs)
  where
    foo []        = [xs]
    foo ((a,b):_) = a : splitBy sep (strip sep b)

    strip [] bs = bs
    strip _  [] = []
    strip (a:as) (b:bs) = if a==b then strip as bs else b:bs

splitBy' :: (Eq a) => [a] -> [a] -> [[a]]
splitBy' sep xs = foo $ splitBy sep xs
  where foo (y:ys) = y : map (sep++) ys

joinBy :: [a] -> [[a]] -> [a]
joinBy sep = concat . intersperse sep

padWith :: a -> Int -> [a] -> [a]
padWith fill n xs = take n $ xs ++ (repeat fill)

maxLengths :: [[[a]]] -> [Int]
maxLengths xsss = map maximum $ transpose $ map (padWith 0 m . map length) xsss
  where m = maximum $ map length xsss


main = do
  args               <- getArgs
  (actions, _, errs) <- return $ getOpt Permute options args
  opts               <- foldl (>>=) (return defaultArgs) actions

  {---------------------------------------------------}
  {- If any command line errors, show usage and quit -}
  {---------------------------------------------------}
  _ <- if errs == []
        then return ()
        else showErrors errs >> showUsage >> exitWith (ExitFailure 1)

  {-----------------------------------------}
  {- If --help is set, show usage and quit -}
  {-----------------------------------------}
  _ <- if (helpFlag opts) == True
        then showUsage >> exitWith ExitSuccess
        else return ()

  f <- if (inputFlag opts == True)
        then readFile (inputPath opts)
        else getContents

  let sep = splitBy (delimiterString opts) (separatorString opts)

  putStr (align sep f)
  exitWith ExitSuccess

showUsage     = hPutStrLn stdout (usageInfo "Usage: a [OPTION...]" options)
showErrors es = hPutStrLn stderr (concat es)

data Flag = Flag
 { inputFlag :: Bool, inputPath :: String
 , separatorFlag :: Bool, separatorString :: String
 , delimiterFlag :: Bool, delimiterString :: String
 , helpFlag :: Bool
 } deriving (Eq, Show)

defaultArgs = Flag
 { inputFlag = False,     inputPath = ""
 , separatorFlag = False, separatorString = "& \\\\"
 , delimiterFlag = False, delimiterString = " "
 , helpFlag = False
 }

options :: [OptDescr (Flag -> IO Flag)]
options = 
 [ Option [] ["help"]
     (NoArg (\opt -> return $ opt {helpFlag = True}))
     "show usage"

 , Option ['i'] ["input"]
     (ReqArg (\arg opt -> return $ opt {inputFlag = True, inputPath = arg}) "FILE")
     "input (if not set, use stdin)"

 , Option ['s'] ["separators"]
     (ReqArg (\arg opt -> return $ opt {separatorFlag = True, separatorString = arg}) "STRING")
     "separators (if not set, use & and \\\\)"

 , Option ['d'] ["delimiter"]
     (ReqArg (\arg opt -> return $ opt {delimiterFlag = True, delimiterString = arg}) "STRING")
     "delimiter (if not set, use space)"
 ]