----------------------------------------------------------------------------- -- Copyright 2019, Advise-Me project team. This file is distributed under -- the terms of the Apache License 2.0. For more information, see the files -- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution. ----------------------------------------------------------------------------- -- | -- Maintainer : bastiaan.heeren@ou.nl -- Stability : provisional -- Portability : portable (depends on ghc) -- -- This module defines the parsing (2nd) phase of the assessment pipeline. -- The only function you should need is `parseMath`. -- ----------------------------------------------------------------------------- module Recognize.Parsing.MathParser ( MParser, parseMath, parseSimple', expr ) where import Control.Monad import Data.Char import Data.Either import Data.List import Data.Maybe import Domain.Math.Data.Relation import Domain.Math.Expr.Data import Domain.Math.Expr.Symbols import Ideas.Common.Rewriting import Ideas.Common.View import Ideas.Utils.Parsing import Prelude hiding ((^)) import Recognize.Data.Math import Recognize.Data.MathParserOptions import Recognize.Expr.Functions import Recognize.Expr.Normalform import Text.Parsec (Parsec) import Util.String type MParser = Parsec String MathParserOptions -- | Parses a string and return a list of `Math` objects together with a boolean denoting the usage of chained equations. parseMath :: MathParserOptions -> String -> (Bool, [Math]) parseMath opts s = case parseSimple' opts (many1 chainedEquations) s of Left msg -> (False, [ M s (Left (MathParseError msg)) ]) Right xs -> let (bs,ys) = unzip xs in (or bs, [ M s (Right x) | x <- concat ys ]) -- | Attempts to parse a set of expressions that form a chained equation (x = y = z = ..) -- -- If successful, True is returned together with the list of parsed expressions -- Otherwise, False is returned with an empty list. chainedEquations :: MParser (Bool, [Expr]) chainedEquations = expr >>= chainedEquations' False chainedEquations' :: Bool -> Expr -> MParser (Bool, [Expr]) chainedEquations' alreadyChained x = do -- decide on the relation type rel <- choice [notEqual, equal, lessThanOrEqualTo, lessThan, greaterThanOrEqualTo, greaterThan] -- parse a new expression y <- expr -- parse the rest of the chained equation (gotChained, eqs) <- chainedEquations' True (if isVariable x then x else y) -- Sometimes people write chained equations where they simplify an expression and then expand it at the same time. -- In that case usually the left most symbol is simplified: (5 + 8 = 13 * 3 = 39 + 4 = ..) let y_l = fromMaybe y (getMostLeft y) let y' = if nf y_l == nf x && not (isVar y_l) then y_l else y return (alreadyChained || gotChained, toExpr (makeType rel x y') : eqs) <|> return (False, [ x | not alreadyChained ]) equal :: MParser RelationType equal = EqualTo <$ ( (try (spstring "==" >> return '=') <|> try (spchar '=' <* spaces <* spchar '=') <|> spchar '=') <* spaces ) notEqual :: MParser RelationType notEqual = NotEqualTo <$ (spstring "/=" <|> spstring "=/=") <* spaces lessThan :: MParser RelationType lessThan = LessThan <$ spchar '<' <* spaces lessThanOrEqualTo :: MParser RelationType lessThanOrEqualTo = LessThanOrEqualTo <$ spstring "<=" <* spaces greaterThan :: MParser RelationType greaterThan = GreaterThan <$ spchar '>' <* spaces greaterThanOrEqualTo :: MParser RelationType greaterThanOrEqualTo = GreaterThanOrEqualTo <$ spstring ">=" <* spaces expr :: MParser Expr expr = buildExpressionParser exprTable (term <* spaces) term :: MParser Expr term = foldl1 (*) <$> factor factor :: MParser [Expr] factor = do n <- number mx <- optionMaybe factor2 return (n : fromMaybe [] mx) <|> factor2 factor2 :: MParser [Expr] factor2 = do xs <- many1 atom mn <- optionMaybe number return (xs ++ maybeToList mn) -- | Parse a single expression -- -- Does not actually parse every atom (numbers and perhaps more) due to legacy reasons and no time to improve. atom :: MParser Expr atom = getState >>= \opts -> choice [ -- Parse a function call: f(x) try $ Sym functionCallSymbol <$> (choice (map spchar (fcallChars opts)) *> parens ((number <|> variable <$> var) `sepBy` (spchar ',' <* spaces))) , (\x y -> Sym rootSymbol [x,y]) <$ spstring "root" <* spaces <*> expr <* spaces <*> expr , variable <$> var , parens (spaces *> expr <* spaces) , brackets (spaces *> expr <* spaces) , braces (spaces *> expr <* spaces) ] where fcallChars opts = ['f', 'g', 'h', 'F', 'G', 'H'] ++ functionCallWhitelist opts exprTable :: [[Operator Char MathParserOptions Expr]] exprTable = [ -- precedence level 9+ [ Prefix (negate <$ spchar '-' <* spaces) , Prefix (id <$ spchar '+' <* spaces) ] , [ Prefix (Sqrt <$ spstring "sqrt" <* spaces) ] , -- precedence level 7 [ Infix ((^) <$ spchar '^' <* spaces) AssocRight ] -- precedence level 7 , [ Infix ((*) <$ spchar '*' <* spaces) AssocLeft -- , Infix ((*) <$ spchar 'x' <* spaces) AssocLeft -- now done by preprocessing -- , Infix ((*) <$ spchar 'X' <* spaces) AssocLeft , Infix ((/) <$ spchar '/' <* spaces) AssocLeft , Infix ((/) <$ spchar ':' <* spaces) AssocLeft ] -- precedence level 6 , [ Infix ((+) <$ spchar '+' <* spaces) AssocLeft , Infix ((-) <$ spchar '-' <* spaces) AssocLeft , Infix ((-) <$ spchar '–' <* spaces) AssocLeft -- this is a different minus sign ( –- ) ] ] -------------------------------------------------------------------------- -- Lexing var :: MParser String var = getState >>= \opts -> if multByConcatenation opts then try $ do c <- satisfy isAlpha when (c `elem` ("fghFGH" ++ functionCallWhitelist opts)) $ notFollowedBy (spchar '(') -- done by preprocessing return [c] else many1 letter parens :: MParser a -> MParser a parens = between (spchar '(') (spchar ')') brackets :: MParser a -> MParser a brackets = between (spchar '[') (spchar ']') braces :: MParser a -> MParser a braces = between (spchar '{') (spchar '}') spchar :: Char -> MParser Char spchar c | c == '/' = try (char c <* notFollowedBy (char '=')) | otherwise = char c spstring :: String -> MParser String spstring s = try (string s) number :: MParser Expr number = digitalNumber <|> specialNumber digitalNumber :: MParser Expr digitalNumber = do xs <- many1 digit m <- optionMaybe (try ((char '.' <|> char ',') *> many1 digit)) case m of Just ys -> return (toExpr (read (xs ++ "." ++ ys) :: Double)) Nothing -> return (toExpr (read xs :: Int)) specialNumber :: MParser Expr specialNumber = (1 :/: 2) <$ string "½" <|> (3 :/: 4) <$ string "¾" <|> (1 :/: 4) <$ string "¼" -- todo: parse a² and a³ -- | Parse until eof complete' :: MParser a -> MParser a complete' p = spaces *> (p <* eof) parseSimple' :: MathParserOptions -> MParser a -> String -> Either String a parseSimple' opts p = left show . runParser (complete' p) opts "" . (if convertToLowercase opts then strToLower else id)