packages feed

setdown (empty) → 0.1.0.0

raw patch · 18 files changed

+1255/−0 lines, 18 filesdep +arraydep +basedep +bytestringsetup-changed

Dependencies added: array, base, bytestring, cmdargs, containers, directory, filepath, mtl, split, text, uuid

Files

+ Context.hs view
@@ -0,0 +1,35 @@+module Context +   ( Context(..)+   , standardContext+   , smallContext+   , prepareContext+   , inOutput+   ) where++import Data.Int+import System.FilePath ((</>))+import System.Directory (createDirectoryIfMissing)++data Context = Context +   { cBaseDir :: FilePath+   , cOutputDir :: FilePath+   , cFilesPerMerge :: Int+   , cBytesPerFileSplit :: Int64+   } deriving (Show, Eq)++smallContext :: Context+smallContext = standardContext { cBytesPerFileSplit = 1000 }++standardContext :: Context+standardContext = Context+   { cBaseDir = "./"+   , cOutputDir = "output"+   , cFilesPerMerge = 10+   , cBytesPerFileSplit = 100 * 1024 * 1024+   }++prepareContext :: Context -> IO ()+prepareContext ctx = createDirectoryIfMissing True (cOutputDir ctx)++inOutput :: Context -> FilePath -> FilePath+inOutput ctx fp = cOutputDir ctx </> fp
+ DefinitionHelpers.hs view
@@ -0,0 +1,27 @@+module DefinitionHelpers +   ( extractIdsFromDefinitions +   , extractFilenamesFromDefinitions+   , extractDefinedIds+   ) where++import SetData+import qualified Data.Set as S++extractIdsFromExpression :: Expression -> S.Set Identifier+extractIdsFromExpression (BinaryExpression _ a b) = extractIdsFromExpression a `S.union` extractIdsFromExpression b+extractIdsFromExpression (FileExpression _) = S.empty+extractIdsFromExpression (IdentifierExpression i) = S.singleton i++extractIdsFromDefinitions :: Definitions -> S.Set Identifier+extractIdsFromDefinitions = foldr S.union S.empty . fmap (extractIdsFromExpression . definitionExpression)++extractFilenamesFromExpression :: Expression -> S.Set FilePath+extractFilenamesFromExpression (BinaryExpression _ a b) = extractFilenamesFromExpression a `S.union` extractFilenamesFromExpression b+extractFilenamesFromExpression (FileExpression fn) = S.singleton fn+extractFilenamesFromExpression (IdentifierExpression _) = S.empty++extractFilenamesFromDefinitions :: Definitions -> S.Set FilePath+extractFilenamesFromDefinitions = foldr S.union S.empty . fmap (extractFilenamesFromExpression . definitionExpression)++extractDefinedIds :: Definitions -> S.Set Identifier+extractDefinedIds = S.fromList . fmap definitionId
+ DuplicateElimination.hs view
@@ -0,0 +1,72 @@+module DuplicateElimination +   ( eliminateDuplicates+   , orderDefinitions+   ) where++import SetData+import Data.Ord (comparing)+import Data.List (sort, sortBy, groupBy, partition)+import Data.Function (on)+import qualified Data.Set as S++eliminateDuplicates :: SimpleDefinitions -> SimpleDefinitions +eliminateDuplicates sds = sort . flip updateWithDuplicates sds $ findDuplicates sds++updateWithDuplicates :: [Duplicates] -> SimpleDefinitions -> SimpleDefinitions+updateWithDuplicates ds sds = S.toList $ foldr updateWithDuplicate (S.fromList sds) ds++-- TODO we now want to find all of the duplicates+-- There are three scenarios that we need to consider:+-- 0 Retain dupes: we can randomly select a node to be the remainder and replace references to the+-- rest+-- 1 Retain dupes: we select the retain dupe and point all of the other dupes to it+-- 2+ Retain dupes: we select a retain dupe and point the other retain dupes to it and replace all+-- of the other dupes with it+updateWithDuplicate :: Duplicates -> S.Set SimpleDefinition -> S.Set SimpleDefinition+updateWithDuplicate (Duplicates [] []) sds = sds+updateWithDuplicate dupes sds = S.insert retainDupe cleanedDefs+   where+      cleanedDefs = S.map (replaceIds newId oldIds) remainingDefs+      remainingDefs = sds S.\\ S.fromList discardDupes+      newId = sdId retainDupe+      oldIds = S.fromList . fmap sdId $ discardDupes+      (retainDupe : discardDupes) = dupRetain dupes ++ dupDiscontinue dupes++replaceIds :: Identifier -> S.Set Identifier -> SimpleDefinition -> SimpleDefinition+replaceIds newId oldIds (SimpleDefinition ident expr retain) = SimpleDefinition ident (replaceIdsInExpression newId oldIds expr) retain++replaceIdsInExpression :: Identifier -> S.Set Identifier -> SimpleExpression -> SimpleExpression+replaceIdsInExpression newId oldIds (SimpleUnaryExpression be) = SimpleUnaryExpression (replaceIdsInBaseExpression newId oldIds be)+replaceIdsInExpression newId oldIds (SimpleBinaryExpression op left right) = SimpleBinaryExpression op (replace left) (replace right)+   where+      replace = replaceIdsInBaseExpression newId oldIds++replaceIdsInBaseExpression :: Identifier -> S.Set Identifier -> BaseExpression -> BaseExpression+replaceIdsInBaseExpression _ _ be@(BaseFileExpression {}) = be+replaceIdsInBaseExpression newId oldIds be@(BaseIdentifierExpression ident) = if ident `S.member` oldIds then BaseIdentifierExpression newId else be++findDuplicates :: SimpleDefinitions -> [Duplicates]+findDuplicates = fmap toDuplicates . filter (\x -> length x > 1) . groupBy ((==) `on` sdExpression) ++toDuplicates :: SimpleDefinitions -> Duplicates+toDuplicates = uncurry Duplicates . partition sdRetain++data Duplicates = Duplicates+   { dupRetain :: SimpleDefinitions+   , dupDiscontinue :: SimpleDefinitions+   } deriving (Show)++orderDefinitions :: SimpleDefinitions -> SimpleDefinitions+orderDefinitions = sortBy (comparing sdExpression) . fmap orderDefinition++orderDefinition :: SimpleDefinition -> SimpleDefinition +orderDefinition sd = sd { sdExpression = orderExpression . sdExpression $ sd }++orderExpression :: SimpleExpression -> SimpleExpression+orderExpression se@(SimpleUnaryExpression {}) = se+orderExpression se@(SimpleBinaryExpression op left right) = if operatorIsCommutative op && right < left then SimpleBinaryExpression op right left else se++operatorIsCommutative :: Operator -> Bool+operatorIsCommutative And = True+operatorIsCommutative Or = True+operatorIsCommutative Difference = False
+ ExpressionConversion.hs view
@@ -0,0 +1,68 @@+module ExpressionConversion +   ( complexToSimpleDefinitions+   ) where++import SetData+import DefinitionHelpers+import Control.Monad.State+import qualified Data.Text.Lazy as T+import Data.Maybe (catMaybes)+import qualified Data.Set as S++data TransitionState = TS+   { tsCount :: Integer+   , tsUsedIds :: S.Set Identifier+   } deriving (Show)++type ConvState = State TransitionState++complexToSimpleDefinitions :: Definitions -> SimpleDefinitions+complexToSimpleDefinitions defs = evalState (fromDefinitions defs) initialState+   where+      initialState = TS +         { tsCount = 0+         , tsUsedIds = extractDefinedIds defs+         }++fromDefinitions :: Definitions -> ConvState SimpleDefinitions+fromDefinitions defs = do +   definitions <- mapM fromDefinition defs+   return . concat $ definitions++fromDefinition :: Definition -> ConvState SimpleDefinitions+fromDefinition (Definition ident expression) = do +   (expr, conv) <- convertExpression expression+   return $ SimpleDefinition ident expr True : conv++convertExpression :: Expression -> ConvState (SimpleExpression, SimpleDefinitions)+convertExpression (IdentifierExpression ident) = return (SimpleUnaryExpression (BaseIdentifierExpression ident), [])+convertExpression (FileExpression filePath) = return (SimpleUnaryExpression (BaseFileExpression filePath), [])+convertExpression (BinaryExpression op left right) = do+   (leftExp, leftConv) <- convertExpression left+   (rightExp, rightConv) <- convertExpression right+   (leftBase, leftDef) <- defFromExpression leftExp+   (rightBase, rightDef) <- defFromExpression rightExp+   return (SimpleBinaryExpression op leftBase rightBase, catMaybes [leftDef, rightDef] ++ leftConv ++ rightConv)++defFromExpression :: SimpleExpression -> ConvState (BaseExpression, Maybe SimpleDefinition)+defFromExpression (SimpleUnaryExpression be) = return (be, Nothing)+defFromExpression se@(SimpleBinaryExpression {}) = do+   defId <- getIdThenIncrement+   let newDef = SimpleDefinition defId se False+   return (BaseIdentifierExpression defId, Just newDef)++getIdThenIncrement :: ConvState Identifier+getIdThenIncrement = do+   original <- get+   let (intId, ident) = head . filter (\x -> snd x `S.notMember` tsUsedIds original) . fmap withId . thisAndFurther . tsCount $ original+   put $ original { tsCount = intId + 1 }+   return ident++withId :: Integer -> (Integer, Identifier)+withId x = (x, integerToId x)++thisAndFurther :: Integer -> [Integer]+thisAndFurther = iterate (+1)++integerToId :: Integer -> Identifier+integerToId = T.pack . show
+ ExternalSort.hs view
@@ -0,0 +1,105 @@+module ExternalSort +   ( extractAndSortFiles +   , extractAndSortFile +   , splitSortAndMerge+   ) where++import Data.List (sort, groupBy)+import Data.Int+import Data.Maybe (catMaybes)+import qualified Data.UUID.V4 as UUID+import Data.List.Split (chunksOf)+import Control.Monad (forM)+import System.FilePath ((</>))+import Control.Arrow (second)+import Control.Applicative+import qualified Data.Text.Lazy as T+import qualified Data.Text.Lazy.IO as T++import Context++extractAndSortFiles :: Context -> [FilePath] -> IO [(FilePath, FilePath)]+extractAndSortFiles ctx = mapM (extractAndSortFile ctx)++extractAndSortFile :: Context -> FilePath -> IO (FilePath, FilePath)+extractAndSortFile ctx fp = do+   resultFile <- splitSortAndMerge ctx fp+   return (fp, resultFile)++splitSortAndMerge :: Context -> FilePath -> IO FilePath+splitSortAndMerge ctx fp = mergeFiles ctx =<< splitAndSort ctx fp++splitAndSort :: Context -> FilePath -> IO [FilePath]+splitAndSort ctx fp = do+   splits <- splitFile ctx fp+   mapM simpleFileSort splits++simpleFileSort :: FilePath -> IO FilePath+simpleFileSort fp = do +   T.writeFile sfp . T.unlines . sort . T.lines =<< T.readFile fp+   return sfp+   where+      sfp = fp ++ ".sorted"++accLines :: [T.Text] -> [(T.Text, Int64)]+accLines = go 0+   where+      go :: Int64 -> [T.Text] -> [(T.Text, Int64)]+      go _     []     = []+      go start (x:xs) = (x, start) : go (start + T.length x) xs++chunkGroup :: Integral a => a -> (b, a) -> (b, a)+chunkGroup size = second (`div` size)++sameChunk :: Eq b => (a, b) -> (a, b) -> Bool+sameChunk (_, a) (_, b) = a == b++splitFile :: Context -> FilePath -> IO [FilePath]+splitFile ctx inputFile = do+   contents <- T.readFile (withBaseDir ctx inputFile)+   -- TODO replace constants with the context+   let splitContents = fmap (T.unlines . fmap fst) . groupBy sameChunk . fmap (chunkGroup . cBytesPerFileSplit $ ctx) . accLines . T.lines $ contents+   let namesAndContents = zip names splitContents+   sequence_ . fmap (uncurry T.writeFile) $ namesAndContents+   return . fmap fst $ namesAndContents+   where+      names = map (\num -> cOutputDir ctx </> inputFile ++ "." ++ show num ++ ".split") [(1 :: Integer)..]++mergeFiles :: Context -> [FilePath] -> IO FilePath+mergeFiles _ [] = error "Tried to merge no files..."+mergeFiles _ [f] = return f -- TODO rename to something sensible+mergeFiles ctx fps = do+   let mergeFilepaths = chunksOf (cFilesPerMerge ctx) fps -- [[FilePath]]+   mergedFiles <- forM mergeFilepaths (directMergeFiles ctx)+   mergeFiles ctx mergedFiles++directMergeFiles :: Context -> [FilePath] -> IO FilePath+directMergeFiles ctx fps = do+   fileContents <- fmap T.lines <$> mapM T.readFile fps+   randomFilename <- fmap show UUID.nextRandom+   let newFile = cOutputDir ctx </> randomFilename+   writeFileLines newFile . merge $ fileContents+   return newFile++writeFileLines :: FilePath -> [T.Text] -> IO ()+writeFileLines f = T.writeFile f . T.unlines++merge :: [[T.Text]] -> [T.Text]+merge xs = case minHead xs of+   Nothing -> []+   Just x -> x : merge (remainders x xs)++remainders :: T.Text -> [[T.Text]] -> [[T.Text]]+remainders x = fmap (dropWhile (== x))++minHead :: [[T.Text]] -> Maybe T.Text+minHead fs = case catMaybes $ fmap safeHead fs of+   [] -> Nothing+   xs -> Just . minimum $ xs++safeHead :: [a] -> Maybe a+safeHead []    = Nothing+safeHead (x:_) = Just x++withBaseDir :: Context -> FilePath -> FilePath+withBaseDir ctx fp = cBaseDir ctx </> fp
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2015, Robert Massaioli++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Robert Massaioli nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Main.hs view
@@ -0,0 +1,203 @@+{-# LANGUAGE DeriveDataTypeable #-}+module Main where++import System.Console.CmdArgs+import qualified Data.ByteString.Lazy as B+import qualified Data.Text.Lazy.IO as T+import qualified Data.Set as S+import System.Exit++import Control.Monad (unless, forM_, filterM)+import Data.List (intersperse, partition)+import Data.Maybe (fromMaybe)+import Control.Applicative++import SetData+import SetInput+import SetInputVerification+import Context+import ExternalSort+import PrintDefinition+import ExpressionConversion+import DefinitionHelpers++import DuplicateElimination+import PerformOperations+import SimpleDefinitionCycles++import System.Directory (doesFileExist)+import System.FilePath (dropFileName, (</>))++-- Useful for Print Debugging+-- import Text.Show.Pretty+-- prettyPrint :: Show a => a -> IO ()+-- prettyPrint = putStrLn . ppShow+++data Options = Options+   { outputDirectory :: Maybe FilePath+   , setdownFile :: FilePath+   } deriving (Show, Data, Typeable)++options :: Options+options = Options +   { outputDirectory = def +      &= explicit +      &= name "output" +      &= typDir +      &= help "The directory in which to place the output contents. Relative to your .setdown file." +      &= opt "output" +   , setdownFile = def+      &= typ "definitions.setdown"+      &= argPos 0+   }+   &= program "setdown"+   &= summary "setdown allows you to perform set operations on multiple files efficiently using an intuitive language."++-- TODO the setdownFile should be optional, at which point we should search the current directory+-- for one+main :: IO ()+main = do+   opts <- cmdArgs options++   let inputFilePath = setdownFile opts+   inputFileExists <- doesFileExist inputFilePath +   unless inputFileExists $ do+      putStrLn $ "Error: The given setdown file did not exist: " ++ inputFilePath+      exitWith (ExitFailure 1)++   -- Todo work out the parent directory of the setdown file+   putStrLn "==> Creating the environment..."+   let baseDir = dropFileName inputFilePath++   let context = standardContext +                  { cBaseDir = baseDir +                  , cOutputDir = baseDir </> fromMaybe "output" (outputDirectory opts)+                  }++   putStrLn $ "Base Directory: " ++ cBaseDir context+   putStrLn $ "Output Directory: " ++ cOutputDir context+   prepareContext context+   printNewline++   setData <- parse <$> (B.readFile . setdownFile $ opts)+   putStrLn "==> Parsed original definitions..."+   printDefinitions setData+   -- Step 0: Verify that the definitions are well defined and that the referenced files exist+   -- relative to the file that we pass in.+   printNewline++   putStrLn "==> Verification (Ensuring correctness in the set definitions file)"+   case duplicateDefinitionName setData of+      [] -> putStrLn "OK: No duplicate definitions found."+      xs -> do +         putStrLn "[Error 11] Duplicate definitions found:"+         mapM_ T.putStrLn xs+         exitWith (ExitFailure 11)++   case unknownIdentifier setData of+      [] -> putStrLn "OK: No unknown identifiers found."+      xs -> do+         putStrLn "[Error 12] Unknown identifiers used in the set descriptor file:"+         mapM_ T.putStrLn xs+         exitWith (ExitFailure 12)++   allFiles <- filesNotFound . S.toList . extractFilenamesFromDefinitions $ setData+   unless (null allFiles) $ do+      putStrLn "[Error 13] the following files could not be found:"+      forM_ allFiles (\fp -> putStrLn $ " - " ++ fp)+      exitWith (ExitFailure 13)+   putStrLn "OK: All files in the definitions could be found."+   printNewline++   putStr "==> Simplifying and eliminating duplicates from set definitions..."+   let simpleSetData = eliminateDuplicates . orderDefinitions . complexToSimpleDefinitions $ setData+   putStrLn "DONE:"+   printSimpleDefinitions simpleSetData+   printNewline++   putStr "==> Checking for cycles in the simplified definitions..."+   let cycles = getCyclesInSimpleDefinitions simpleSetData+   putStrLn "DONE:"+   unless (null cycles) $ do+      putStrLn "[Error 20] found cyclic dependencies in the definitions!"+      printNewline+      putStrLn "We found the following cycles:"+      printCycles cycles+      exitWith (ExitFailure 20)+   putStrLn "OK: No cycles were found in the definitions."+   printNewline++   putStrLn "==> Copying and Sorting all input files from the definitions..."+   -- Step 1: For every unique file, sort it (Use external sort for this purpose:+   -- https://hackage.haskell.org/package/external-sort-0.2/docs/Algorithms-ExternalSort.html add+   -- docs to that library if at all possible)+   -- TODO use file timestamps to not sort these big files more than once if possible+   sortedFiles <- extractAndSortFiles context (S.toList . extractFilenamesFromDefinitions $ setData) -- TODO use the simple set data here+   printSortResults sortedFiles+   printNewline++   putStrLn "==> Computing set operations between the files..."+   -- Step 2: Calculate the graph of everything that needs to be computed and compute things one at+   -- a time. Even make sure that you store the temporary results along the way. That way we can+   -- refer to them later if the same computation is made twice. We should certainly memoize with+   -- the file system. It would be great if we could print out the results of the computations as we+   -- go.+   computedFiles <- runSimpleDefinitions context simpleSetData sortedFiles+   -- Step 3: Print out the final statistics with the defitions pointing to how many elements that+   -- each contained and where to find their output files.+   printComputedResults computedFiles+   +filesNotFound :: [FilePath] -> IO [FilePath]+filesNotFound = filterM (\x -> not <$> doesFileExist x)++printCycles :: [SimpleDefinitions] -> IO ()+printCycles sds = forM_ sds $ \sd -> do+   putStr "   "+   printCycle sd+   printNewline++printCycle :: SimpleDefinitions -> IO ()+printCycle [] = putStrLn "Not a cycle."+printCycle (x:xs) = sequence_ . intersperse (putStr " -> ") $ printIdentifiers+   where+      printIdentifiers = fmap (printIdentifier . sdId) loopRound+      loopRound = [x] ++ xs ++ [x]++-- TODO use the box library to print these items in a nice tabulated way+printSortResults :: [(FilePath, FilePath)] -> IO ()+printSortResults = sequence_ . fmap printSortResult++printSortResult :: (FilePath, FilePath) -> IO ()+printSortResult (unsortedFile, sortedFile) = do+   putStr . wrapInQuotes $ unsortedFile+   putStr " (unsorted) => "+   putStr . wrapInQuotes $ sortedFile +   putStrLn " (sorted)"+   where+      wrapInQuotes x = "\"" ++ x ++ "\""++printComputedResults :: [(SimpleDefinition, FilePath)] -> IO ()+printComputedResults results = do+   unless (null tempResults) $ do+      putStrLn "Transient results:"+      printResults tempResults+      printNewline+   unless (null retainResults) $ do+      putStrLn "Required results:"+      printResults retainResults +   where+      (retainResults, tempResults) = partition (sdRetain . fst) results+      printResults = sequence_ . intersperse printNewline . fmap printComputedResult ++printComputedResult :: (SimpleDefinition, FilePath) -> IO ()+printComputedResult (SimpleDefinition ident _ _, fp) = do+   printIdentifier ident+   putStr ": "+   putStrLn fp++printIdentifier :: Identifier -> IO ()+printIdentifier = T.putStr++printNewline :: IO ()+printNewline = putStrLn ""
+ PerformOperations.hs view
@@ -0,0 +1,118 @@+module PerformOperations (runSimpleDefinitions) where++import Control.Arrow (first)+import Control.Applicative+import Control.Monad.State.Lazy+import Context+import SetData+import qualified Data.Text.Lazy as T+import qualified Data.Text.Lazy.IO as T+import qualified Data.Map as M+import qualified Data.UUID.V4 as UUID++data ComputeState = CS+   { expressionToFile :: M.Map BaseExpression FilePath+   , definitionMap :: M.Map Identifier SimpleDefinition+   , csContext :: Context+   }++runSimpleDefinitions :: Context -> SimpleDefinitions -> [(FilePath, FilePath)] -> IO [(SimpleDefinition, FilePath)]+runSimpleDefinitions context defs sortedFileMapping = fst <$> runStateT (computeSimpleDefinitions defs) cs+   where+      cs = CS+         { expressionToFile = setupExpressionsToFile sortedFileMapping +         , definitionMap = toDefinitionMap defs+         , csContext = context+         }+      +toDefinitionMap :: SimpleDefinitions -> M.Map Identifier SimpleDefinition+toDefinitionMap = M.fromList . fmap (\x -> (sdId x, x))++setupExpressionsToFile :: [(FilePath, FilePath)] -> M.Map BaseExpression FilePath+setupExpressionsToFile = M.fromList . fmap (first BaseFileExpression)++computeSimpleDefinitions :: SimpleDefinitions -> StateT ComputeState IO [(SimpleDefinition, FilePath)]+computeSimpleDefinitions = mapM csd+   where+      csd :: SimpleDefinition -> StateT ComputeState IO (SimpleDefinition, FilePath)+      csd sd = do+         resultFile <- computeSimpleDefinition sd+         return (sd, resultFile)++-- Always return the file that should be used for the next computation+computeSimpleDefinition :: SimpleDefinition -> StateT ComputeState IO FilePath+computeSimpleDefinition (SimpleDefinition ident (SimpleUnaryExpression be) _) = do+   newFile <- computeBaseExpression be+   mapIdToFile ident newFile+   return newFile+computeSimpleDefinition (SimpleDefinition ident (SimpleBinaryExpression op left right) _) = do+   leftFile <- computeBaseExpression left+   rightFile <- computeBaseExpression right+   ctx <- csContext <$> get+   resultFile <- lift $ fileSetOperation ctx op leftFile rightFile+   mapIdToFile ident resultFile+   return resultFile++computeBaseExpression :: BaseExpression -> StateT ComputeState IO FilePath+computeBaseExpression be@(BaseFileExpression fp) = do +   expressionMap <- expressionToFile <$> get+   case M.lookup be expressionMap of+      Just sortedFile -> return sortedFile+      Nothing -> fail $ "Could not find a sorted file for: " ++ fp+computeBaseExpression be@(BaseIdentifierExpression ident) = do+   cs <- get+   case M.lookup be (expressionToFile cs) of+      Just preComputedFile -> return preComputedFile+      Nothing -> case M.lookup ident (definitionMap cs) of+         Nothing -> fail $ "Could not find a definition for the identifier: " ++ T.unpack ident+         Just def -> computeSimpleDefinition def++mapIdToFile :: Monad a => Identifier -> FilePath -> StateT ComputeState a ()+mapIdToFile ident fp = modify (\currentState -> currentState+   { expressionToFile = M.insert (BaseIdentifierExpression ident) fp (expressionToFile currentState)+   })++fileSetOperation :: Context -> Operator -> FilePath -> FilePath -> IO FilePath+fileSetOperation ctx ot leftFp rightFp = do+   leftContents <- T.lines <$> T.readFile leftFp +   rightContents <- T.lines <$> T.readFile rightFp +   let mergedContents = linesSetOperation (operatorTools ot) leftContents rightContents +   randomFilename <- randomFilenameInOutput ctx+   T.writeFile randomFilename . T.unlines $ mergedContents+   return randomFilename+   +randomFilenameInOutput :: Context -> IO FilePath+randomFilenameInOutput ctx = inOutput ctx . show <$> UUID.nextRandom++linesSetOperation :: OperatorTools T.Text -> [T.Text] -> [T.Text] -> [T.Text]+linesSetOperation ot = go+   where +      go :: [T.Text] -> [T.Text] -> [T.Text]+      go [] [] = []+      go xs [] = if otKeepRemainderLeft ot then xs else []+      go [] xs = if otKeepRemainderRight ot then xs else []+      go left@(l:ls) right@(r:rs) = +         if (otCompare ot) l r +            then chosen : go (dropWhileChosen left) (dropWhileChosen right)+            else case compare l r of+               LT -> go ls   right+               EQ -> go ls   rs+               GT -> go left rs+         where+            chosen = (otChoose ot) l r+            dropWhileChosen = dropWhile (== chosen)+      +data (Eq a, Ord a) => OperatorTools a = OT+   { otCompare :: a -> a -> Bool+   , otChoose  :: a -> a -> a+   , otKeepRemainderLeft :: Bool+   , otKeepRemainderRight :: Bool+   } ++operatorTools :: Ord a => Operator -> OperatorTools a+operatorTools And          = OT (==)            const False False -- fst or snd, it does not matter they are equal+operatorTools Or           = OT (const2 True)   min True True+operatorTools Difference   = OT (<)             const True False ++const2 :: a -> b -> c -> a+const2 = const . const
+ PrintDefinition.hs view
@@ -0,0 +1,85 @@+module PrintDefinition+   ( printDefinitions+   , printDefinition+   , printSimpleDefinitions +   , printSimpleDefinition+   ) where++import SetData+import Data.List (intersperse)+import qualified Data.Text.Lazy.IO as T++-- Complex Definition Printing+printDefinitions :: Definitions -> IO ()+printDefinitions = sequence_ . intersperse printNewline . fmap printDefinition ++printDefinition :: Definition -> IO ()+printDefinition (Definition ident expression) = do+   printId ident+   putStr ": "+   printExpression expression+   printNewline++printExpression :: Expression -> IO ()+printExpression (FileExpression fp) = putStr $ "\"" ++ fp ++ "\""+printExpression (IdentifierExpression ident) = printId ident+printExpression (BinaryExpression op left right) = do+   printSubExpression left+   putStr " "+   printOperator op+   putStr " "+   printSubExpression right+   where+      printSubExpression expr = maybeWrapInBrackets (isBinaryExpression expr) (printExpression expr)+++-- Simple Definition Printing+printSimpleDefinitions :: SimpleDefinitions -> IO ()+printSimpleDefinitions = sequence_ . intersperse printNewline . fmap printSimpleDefinition++printSimpleDefinition :: SimpleDefinition -> IO ()+printSimpleDefinition (SimpleDefinition ident se _) = do+   printId ident+   putStr ": "+   printSimpleExpression se+   printNewline++printSimpleExpression :: SimpleExpression -> IO ()+printSimpleExpression (SimpleUnaryExpression be) = printBaseExpression be+printSimpleExpression (SimpleBinaryExpression op left right) = do+   printBaseExpression left+   putStr " "+   printOperator op+   putStr " "+   printBaseExpression right++printBaseExpression :: BaseExpression -> IO ()+printBaseExpression (BaseIdentifierExpression ident) = printId ident+printBaseExpression (BaseFileExpression fp) = putStr $ "\"" ++ fp ++ "\""++-- Common Printing Code+maybeWrapInBrackets :: Bool -> IO () -> IO ()+maybeWrapInBrackets True  = wrapInBrackets+maybeWrapInBrackets False = id++wrapInBrackets :: IO () -> IO ()+wrapInBrackets printAction = do+   putStr "("+   printAction+   putStr ")"++printOperator :: Operator -> IO ()+printOperator And          = putStr "/\\"+printOperator Or           = putStr "\\/"+printOperator Difference   = putStr "-"++printId :: Identifier -> IO ()+printId = T.putStr++printNewline :: IO ()+printNewline = putStrLn ""++-- TODO Move this out to a utility class+isBinaryExpression :: Expression -> Bool+isBinaryExpression (BinaryExpression {}) = True+isBinaryExpression _ = False
+ README.markdown view
@@ -0,0 +1,153 @@+# Setdown - Line based set manipulation++Author: [Robert Massaioli][6]  +Created in: 2015  ++## What is setdown and how does it work?++Setdown is a command line tool for line based set operations. To use setdown you write a "setdown+definitions file" often suffixed with **.setdown**. If you are familiar with [Make][3] then you can think+of this **.setdown** file much like a Makefile. Inside that file you write a number of+definitions of the form:++    definitionName: "file-1.txt" /\ "file-2.txt"++This line says that "definitionName" is a new set definition that is a label for the intersection of+"file-1.txt" and "file-2.txt". You can write more complicated expressions that this.++### Example Setdown Projects++You should read how setdown works in the sections below, however, if you wish to follow along by+using a few descriptions then you should [checkout the setdown-examples project][2] on BitBucket.++### Input Files++In setdown *each file is treated as a list of elements where each line+is an element*. You may have thought that each file would be treated as a Set of elements (no+duplicate lines). But I decided not to make this a requirement, this is because I don't assume that +you will give us sorted input with no duplicates or that it will be easy for you to do so. Instead+you can give us any file that you like and the first thing that setdown will do to those files is+sort them and remove duplicates; essentially turning them into sets.++Another important point is that of relativity: specifically, if have a **.setdown** file that+references the input file "some-elements.txt" and I run the setdown executable from a directory that+is not the same directory as the **.setdown** file then where will setdown look for the+some-elements.txt file? The answer is that we always look for files relative to the **.setdown**+file. That is where you wrote your definitions so the paths are relative to that. It was designed in+this way so that you could run setdown from anywhere in the directory tree and still get the same+result. It was an important design of setdown that you always get the same result every time that you run it. +Setdown has been designed to be current working directory invariant, as opposed to many+other command line programs. Please keep this in mind. ++### Set Operations and Precidence++In the setdown language there are a number of supported operators:++ - Intersection: /\+ - Union: \/+ - Difference: -++For example, they might be used in the following way:++    definition: (A - B) \/ (C /\ D)++You may be wondering what [operator precidence][1] the setdown language uses and the answer is:+there is no operator precidence at all, instead *you must clearly specify the precidence of nested+expressions with brackets*. This is very important because it will result in parsing errors+otherwise. To show you why I made this decision lets show you an example:++    -- Here is a simple expression+    def: A /\ B \/ C+    -- Now, should this be parsed as:+    defV1: (A /\ B) \/ C+    -- or as:+    defV2: A /\ (B \/ C)+    -- If you pretend that B is the empty set (E) then you can see that these expression evaluate+    -- completely differently. If we simplify them with that assumption then they become:+    defV1-bempty: E+    defV2-bempty: A /\ C++So as you can see, order of operations really matters for set operations. Because it is so critical+I decided to make the use of brackets mandatory. Sorry for the extra brackets but you will thank me+when you expressions come out exactly the way that you expect them to.++### Comments++In the setdown language you can add comments by writing a double-dash (--) and then writing the+comment till the end of the line. The following comments are valid:++    -- This is a definition for A, created because we wanted to do X+    A: "y.txt" - "z.txt"+    +    -- This is an example of a comment halfway through an expression+    B: (A \/ C) -- \/ D This is still a comment and \/ D never happens++You can use comments to leave messages for any people that might read your setdown definitions in+the future. It may help explain to them what you were trying to do.++### Writing your own definitions++In the setdown language you can write a definition in the following format:++     <definitionName>: <expression>++Where the definition name is the identifier that you give to that expression. An expression is the+application of set operations on identifiers or files. A practical example of what this looks like+should help cement what this means. Here is a valid setdown file: ++    -- A is the intersection of the file b-1.out and the set B+    A: "b-1.out" /\ B++    -- B is the union of the file a-1.out and a-2.out+    B: "a-1.out" \/ "a-2.out"++    -- C is the difference of the file b-1.out and the set B+    C: "b-1.out" - B++Usually, when you write these definitions you put them in a file that has a suffix of **.setdown**.+You can then feed this file into the setdown executable like so:++    setdown path/to/mydefinitions.setdown++For more information on the options that you can pass to the setdown executable try running++    setdown --help++And good luck!++## Building the code++To build the code for this project just have [Haskell installed][4] and [cabal][5] and then:++    cabal sandbox init+    cabal install++And that should have the code built on your machine. Then, if you modify the code, just use cabal run to run setdown:++    cabal run -- --help+    cabal run mydefinitions.setdown++That is all that there is to it! ++## Contributing to the setdown project++If you wish to contribute to the setdown project then please just:++ 1. Raise an issue with what you intend to fix / improve.+ 1. Wait for Robert Massioli to get back to you and give you the thumbs up. If you get Robert+    Massaioli's "merge approval" then that means that, if you write the code to Roberts satisfaction+    then it will be merged in.+ 1. Write the code.+ 1. Raise a PR and ask Robert Massaioli to review it. (Maybe iterate a bit to get it cleaned up)+ 1. Get it merged in.+ 1. Celebrate!++I would love to have contributions to the project and, even though it may look like a complicated+process just follow it because it is designed to make your life, and my life, easier. Cheers!++ [1]: http://en.wikipedia.org/wiki/Order_of_operations+ [2]: https://bitbucket.org/robertmassaioli/setdown-examples+ [3]: http://www.gnu.org/software/make/+ [4]: https://www.haskell.org/platform/+ [5]: https://www.haskell.org/cabal/+ [6]: https://robertmassaioli.wordpress.com/
+ SetData.hs view
@@ -0,0 +1,53 @@+module SetData +   ( Definitions+   , Definition(..)+   , Expression(..)+   , Operator(..)+   , Identifier+   , SimpleDefinitions+   , SimpleDefinition(..)+   , SimpleExpression(..)+   , BaseExpression(..)+   ) where++import qualified Data.Text.Lazy as TL++type Definitions = [Definition]++data Definition = Definition+   { definitionId :: Identifier+   , definitionExpression :: Expression+   } deriving (Eq, Show)++data Operator+   = And+   | Or+   | Difference+   deriving (Eq, Ord, Show)++data Expression+   = BinaryExpression Operator Expression Expression+   | FileExpression FilePath+   | IdentifierExpression Identifier+   deriving (Eq, Show)++type Identifier = TL.Text++type SimpleDefinitions = [SimpleDefinition]++data SimpleDefinition = SimpleDefinition+   { sdId :: Identifier+   , sdExpression :: SimpleExpression+   , sdRetain :: Bool -- This is true if this is a definition that must be maintained+   } deriving (Eq, Ord, Show)++data SimpleExpression +   = SimpleBinaryExpression Operator BaseExpression BaseExpression+   | SimpleUnaryExpression BaseExpression+   deriving (Eq, Ord, Show)++data BaseExpression+   = BaseFileExpression FilePath+   | BaseIdentifierExpression Identifier+   deriving (Eq, Ord, Show)+
+ SetInput.hs view
@@ -0,0 +1,26 @@+module SetInput +   ( parse+   ) where++import SetData++import qualified Data.ByteString.Lazy as BL+import qualified SetLanguage as SL+import qualified SetParser as SP++parse :: BL.ByteString -> Definitions+parse = fmap fromParserDefinition . SP.parseSetLanguage . SL.alexScanTokens++fromParserDefinition :: SP.Definition -> Definition+fromParserDefinition (SP.Definition (SP.Identifier name) expression) = Definition name (fromParserExpression expression)++fromParserExpression :: SP.Expression -> Expression+fromParserExpression (SP.BinaryExp op a b) = BinaryExpression (fromParserOperator op) (fromParserExpression a) (fromParserExpression b)+fromParserExpression (SP.BrackExp a) = fromParserExpression a+fromParserExpression (SP.FilenameExp filename) = FileExpression filename+fromParserExpression (SP.IdentifierExp (SP.Identifier name)) = IdentifierExpression name++fromParserOperator :: SP.SetOperator -> Operator+fromParserOperator SP.IntersectionOp = And+fromParserOperator SP.UnionOp = Or+fromParserOperator SP.DifferenceOp = Difference
+ SetInputVerification.hs view
@@ -0,0 +1,30 @@+module SetInputVerification where ++import qualified Data.Text.Lazy as TL+import Data.List+import qualified Data.Set as S+import SetData+import DefinitionHelpers++-- TODO these functions should generate potential lists of error messages++duplicateDefinitionName :: Definitions -> [TL.Text]+duplicateDefinitionName defs = fmap message problems+   where+      message :: [Identifier] -> TL.Text+      message problem = TL.pack "Created multiple definitions for: " `TL.append` (TL.pack . show $ problem)++      problems = filter ((>1) . length) . group . sort . fmap definitionId $ defs++unknownIdentifier :: Definitions -> [TL.Text]+unknownIdentifier defs = fmap message (S.toList nonDefinedIds)+   where+      message :: Identifier -> TL.Text+      message problem = TL.pack "Attempted to use an identifier that has not been defined: " `TL.append` problem++      nonDefinedIds = usedIds S.\\ definitionIds+      usedIds = extractIdsFromDefinitions defs+      definitionIds = extractDefinedIds defs++-- TODO add a function that searches through the list of files in the definition and returns an+-- error for all of the files that do not exist
+ SetLanguage.x view
@@ -0,0 +1,43 @@+{+{-# OPTIONS_GHC -w #-}+module SetLanguage where++import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding as TLE+}++%wrapper "basic-bytestring"++$digit = [0-9]+$nonFilename = [^\\\/\?\%\*\:\|\<\>\ ]+$ident = [a-zA-Z0-9\-\_]++tokens :-++   $white+           ;+   "--".*            ;+   "("               { const LParenTok }+   ")"               { const RParenTok }+   "/\"              { const IntersectionTok }+   "\/"              { const UnionTok }+   "∪"               { const IntersectionTok }+   "∩"               { const UnionTok }+   "-"               { const DifferenceTok }+   \"[^\"]+\"        { FilenameTok . TL.unpack . TL.init . TL.tail . TLE.decodeUtf8 }+   $ident+$white*":" { IdentifierDefinitionTok . TL.strip . TL.init . TLE.decodeUtf8 }+   $ident+           { IdentifierTok . TLE.decodeUtf8 }++{+-- TODO include the location in which the token ocurred so that we can give better error messages+-- later in the parsing+data SetToken+   = FilenameTok FilePath+   | IdentifierDefinitionTok TL.Text+   | IdentifierTok TL.Text+   | IntersectionTok+   | UnionTok+   | DifferenceTok+   | LParenTok+   | RParenTok+   deriving(Show, Eq)+}
+ SetParser.y view
@@ -0,0 +1,70 @@+{+module SetParser where++import SetLanguage+import qualified Data.Text.Lazy as TL+-- TODO add precidence of operators+-- TODO add comments to the language+-- TODO The tokens should come with line number information so that we can better pinpoint errors.+}++%name parseSetLanguage+%tokentype { SetToken }+%error { parseError }++%token+   '('            { LParenTok }+   ')'            { RParenTok }+   and            { IntersectionTok }+   or             { UnionTok }+   diff           { DifferenceTok }+   identifierDef  { IdentifierDefinitionTok $$ }+   identifier     { IdentifierTok $$ }+   filename       { FilenameTok $$ }++%%++definitions : definition               { [$1] }+            | definitions definition   { $2 : $1 }++definition : identifierDef exp         { Definition (Identifier $1) $2 }++exp : baseexp                          { $1 }+    | brackexp                         { $1 }+    | baseorbrack operator baseorbrack { BinaryExp $2 $1 $3 }++baseorbrack : baseexp        { $1 }+            | brackexp       { $1 }++brackexp : '(' exp ')'       { BrackExp $2 }++baseexp : filename           { FilenameExp $1 }+        | identifier         { IdentifierExp (Identifier $1) }++operator : and    { IntersectionOp }+         | or     { UnionOp }+         | diff   { DifferenceOp }++{+parseError :: [SetToken] -> a+parseError tokens = error $ "Parse error: " ++ show tokens++data SetOperator +   = IntersectionOp+   | UnionOp+   | DifferenceOp+   deriving(Show, Eq)++data Identifier = Identifier+   { idName :: TL.Text+   } deriving(Show, Eq)++data Expression+   = BinaryExp SetOperator Expression Expression+   | BrackExp Expression+   | FilenameExp FilePath+   | IdentifierExp Identifier+   deriving(Show, Eq)++data Definition = Definition Identifier Expression deriving (Show, Eq)+}
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ SimpleDefinitionCycles.hs view
@@ -0,0 +1,30 @@+module SimpleDefinitionCycles +   ( getCyclesInSimpleDefinitions+   ) where++import SetData+import qualified Data.Graph as G++getCyclesInSimpleDefinitions :: SimpleDefinitions -> [SimpleDefinitions]+getCyclesInSimpleDefinitions = filter (not . null) . fmap getCycles . simpleDefinitionsToSCC ++getCycles :: G.SCC SimpleDefinition -> SimpleDefinitions+getCycles (G.CyclicSCC verticies) = verticies+getCycles _ = []++simpleDefinitionsToSCC :: SimpleDefinitions -> [G.SCC SimpleDefinition]+simpleDefinitionsToSCC = G.stronglyConnComp . fmap toNode++toNode :: SimpleDefinition -> (SimpleDefinition, Identifier, [Identifier])+toNode sd = (sd, sdId sd, getIdsFromSimpleDefinition sd)++getIdsFromSimpleDefinition :: SimpleDefinition -> [Identifier]+getIdsFromSimpleDefinition (SimpleDefinition _ expr _) = getIdsFromSimpleExpression expr++getIdsFromSimpleExpression :: SimpleExpression -> [Identifier]+getIdsFromSimpleExpression (SimpleBinaryExpression _ left right) = getIdsFromBaseExpression left ++ getIdsFromBaseExpression right+getIdsFromSimpleExpression (SimpleUnaryExpression expr) = getIdsFromBaseExpression expr++getIdsFromBaseExpression :: BaseExpression -> [Identifier]+getIdsFromBaseExpression (BaseIdentifierExpression ident) = [ident]+getIdsFromBaseExpression _ = []
+ setdown.cabal view
@@ -0,0 +1,105 @@+-- Initial setdown.cabal generated by cabal init.  For further +-- documentation, see http://haskell.org/cabal/users-guide/++-- The name of the package.+name:                setdown++-- The package version.  See the Haskell package versioning policy (PVP) +-- for standards guiding when and how versions should be incremented.+-- http://www.haskell.org/haskellwiki/Package_versioning_policy+-- PVP summary:      +-+------- breaking API changes+--                   | | +----- non-breaking API additions+--                   | | | +--- code changes with no API change+version:             0.1.0.0++-- A short (one-line) description of the package.+synopsis:            Treating files as sets to perform rapid set manipulation.++-- A longer description of the package.+description:         There will be times when you have lots of set data and you want to perform many+                     set operations quickly and reliably, you will also want to be able to add new+                     data to your set operations and be able to run the same set operations with +                     little effort. This is the problem that setdown aims to solve. Setdown was+                     built with the intention that you would use it in conjunction with version+                     control tools to manage your set data and set description file.++homepage:            http://bitbucket.org/robertmassaioli/setdown+bug-reports:         https://bitbucket.org/robertmassaioli/setdown/issues++-- The license under which the package is released.+license:             BSD3++-- The file containing the license text.+license-file:        LICENSE++-- The package author(s).+author:              Robert Massaioli++-- An email address to which users can send suggestions, bug reports, and +-- patches.+maintainer:          robertmassaioli@gmail.com++-- A copyright notice.+copyright:           (c) 2015 Robert Massaioli++-- category:            ++build-type:          Simple++-- Extra files to be distributed with the package, such as examples or a +-- README.+extra-source-files:  README.markdown++-- Constraint on the version of Cabal needed to build this package.+cabal-version:       >=1.10++source-repository head+   type: git+   location: git@bitbucket.org:robertmassaioli/setdown.git++executable setdown+  -- .hs or .lhs file containing the Main module.+  main-is:             Main.hs+  +  -- Modules included in this executable, other than Main.+  other-modules:       SetLanguage+                       , SetParser+                       , Context+                       , DefinitionHelpers+                       , DuplicateElimination+                       , ExpressionConversion+                       , ExternalSort+                       , Main+                       , PerformOperations+                       , PrintDefinition+                       , SetData+                       , SetInput+                       , SetInputVerification+                       , SimpleDefinitionCycles+  +  -- LANGUAGE extensions used by modules in this package.+  -- other-extensions:    +  +  -- Other library packages from which modules are imported.+  build-depends:       base            >=4.7 && <4.8+                       -- Lexing dependencies+                       , array         == 0.5.*+                       , bytestring    == 0.10.4.*+                       , text          == 1.2.*+                       -- , pretty-show == 1.6.*+                       -- Module Dependencies+                       , filepath      >= 1.2 && < 2+                       , directory     >= 1.1 && < 2+                       , containers    == 0.5.*+                       , uuid          == 1.3.*+                       , split         == 0.2.*+                       , mtl           == 2.2.*+                       , cmdargs       == 0.10.*+  +  -- Directories containing source files.+  -- hs-source-dirs:      +  +  -- Base language which the package is written in.+  default-language:    Haskell2010++  ghc-options: -Wall