{-# LANGUAGE OverloadedStrings #-} module Exercise04 where import qualified Data.Text as T import Data.Bifunctor (Bifunctor(second)) import Data.Char (isSpace) import Data.List (intercalate) {-WETT-} xmlLight :: String -> Bool xmlLight str = T.pack str `xmlLight'` [] xmlLight' :: T.Text -> [T.Text] -> Bool -- str: Remaining string to parse. -- tagIDs: List of tag IDs of previously opened tags. -- This list is used like a stack, hence the last tag that was opened -- is the head of the list. xmlLight' str tagIDs -- Tip: Have a look at the local definitions in the "where" part for -- a better understanding! -- The string is just plaintext or empty, i. e. is does not contain < or >. -- Therefore, the XML is valid if all tags have been closed, -- i. e. tagIDs is empty. | not $ "<" `T.isInfixOf` str || ">" `T.isInfixOf` str = null tagIDs -- > before the tag | ">" `T.isInfixOf` beforeTag -- no > after a < || not (">" `T.isInfixOf` fromTag) -- tagID contains not only trailing whitespace || T.any isSpace tagID -- Closing tag does not close the last tag that was opened. -- take 1 needs to be called instead of head because the list of opened -- tags might be empty. In that case, False has to be returned because it -- is not allowed to close a tag that was not opened. || tag /= tagID && take 1 tagIDs /= [tagID] = False -- If the parsed tag is an opening tag, add it on top of the tagIDs stack. -- If it was a valid closing tag, remove this tag from the tagIDs stack. -- Call xmlLight' recursively to validate the remaining xml with the -- updated stack. -- It is okay to call tail here, since the previous two lines of code -- assure that tagIDs is not empty. | otherwise = xmlLight' xml $ if tag == tagID then tagID : tagIDs else tail tagIDs where -- beforeTag: string before the first < -- fromTag: string after the first < (beforeTag, fromTag) = split "<" str -- tagWithSpaces: string between < and > -- xml: string after > (tagWithSpaces, xml) = split ">" fromTag -- tag without trailing spaces tag = T.stripEnd tagWithSpaces -- tagID without leading / if the tag is a closing tag. -- It is okay to call T.head here, since the tagID -- will not be empty according to the task. tagID = if T.head tag == '/' then T.tail tag else tag -- Function that splits a text at a given delimiter. -- split calls T.breakOn and removes the leading delimiter from the second -- component of the resulting pair. -- It is okay to use T.tail here because xmlLight' assures that it does not -- use the second component of the result if str does not contain delim. split :: T.Text -> T.Text -> (T.Text, T.Text) split delim str = T.tail `second` T.breakOn delim str {-MCCOMMENT The solution above passes all tests on Artemis, yet it does not pass my own tests. For example, it returns True for "<" although it should return False according to rule 4. Therefore, I've got an alternative solution with six more tokens that can handle such strings properly: xmlLight :: String -> Bool xmlLight str = T.pack str `xmlLight'` [] xmlLight' :: T.Text -> [T.Text] -> Bool -- str: Remaining string to parse. -- tagIDs: List of tag IDs of previously opened tags. -- This list is used like a stack, hence the last tag that was opened -- is the head of the list. xmlLight' str tagIDs -- Tip: Have a look at the local definitions in the "where" part for -- a better understanding! -- The string is just plaintext or empty, i. e. is does not contain < or >. -- Therefore, the XML is valid if all tags have been closed, -- i. e. tagIDs is empty. | not $ "<" `T.isInfixOf` str || ">" `T.isInfixOf` str = null tagIDs -- > before the tag | ">" `T.isInfixOf` beforeTag -- no > after a < || not (">" `T.isInfixOf` fromTag) -- tagID contains no reserved character || T.any (`elem` reservedChars) tagID -- Closing tag does not close the last tag that was opened. -- take 1 needs to be called instead of head because the list of opened -- tags might be empty. In that case, False has to be returned because it -- is not allowed to close a tag that was not opened. || tag /= tagID && take 1 tagIDs /= [tagID] = False -- If the parsed tag is an opening tag, add it on top of the tagIDs stack. -- If it was a valid closing tag, remove this tag from the tagIDs stack. -- Call xmlLight' recursively to validate the remaining xml with the -- updated stack. -- It is okay to call tail here, since the previous two lines of code -- assure that tagIDs is not empty. | otherwise = xmlLight' xml $ if tag == tagID then tagID : tagIDs else tail tagIDs where reservedChars :: String reservedChars = " <>/" -- beforeTag: string before the first < -- fromTag: string after the first < (beforeTag, fromTag) = split "<" str -- tagWithSpaces: string between < and > -- xml: string after > (tagWithSpaces, xml) = split ">" fromTag -- tag without trailing spaces tag = T.stripEnd tagWithSpaces -- tagID without leading / if the tag is a closing tag. -- It is okay to call T.head here, since the tagID -- will not be empty according to the task. tagID = if T.head tag == '/' then T.tail tag else tag -- Function that splits a text at a given delimiter. -- split calls T.breakOn and removes the leading delimiter from the second -- component of the resulting pair. -- It is okay to use T.tail here because xmlLight' assures that it does not -- use the second component of the result if str does not contain delim. split :: T.Text -> T.Text -> (T.Text, T.Text) split delim str = T.tail `second` T.breakOn delim str -} {-TTEW-} {- Test cases tests :: [(String, Bool)] tests = [ -- Rule 1 ("", True), ("", True), ("", True), ("", True), ("", False), ("", False), ("", False), ("", False), ("", False), -- Rule 3 ("", True), ("", False), ("", True), ("< a >", False), ("", True), -- Rule 4 ("<", False), (">", False), ("<", False), ("<<>", False), ("abc", False), ("abc>def", False), ("<", False), ("<", False), (">", False), (">", False), ("<", False), -- Rule 6 ("", True), ("", False), ("", True), -- Rule 7 ("", True), ("abc", True), ("abc", True), ("abcabcabc", True), ("abcabcabcabcabc", True), -- Rule 8 ("", False) ] boolToStr :: Bool -> String boolToStr True = "" boolToStr False = "" testResults :: [String] testResults = filter (not . null) (map (\(xml, expected) -> let actual = xmlLight xml in if actual == expected then "" else xml ++ " \t-- expected " ++ boolToStr expected ++ " but got " ++ boolToStr actual) tests) test :: IO () test | null testResults = putStr "Congratulations! Passed all tests.\n" | otherwise = putStr ("Failed the following tests:\n\t" ++ intercalate "\n\t" testResults ++ "\n") -}