packages feed

countdown-numbers-game-0.0: src/Solve.hs

module Solve where

import qualified Data.NonEmpty as NonEmpty
import qualified Data.List.HT as ListHT
import qualified Data.List as List
import Data.NonEmpty ((!:))
import Data.Traversable (traverse)
import Data.Tuple.HT (mapPair, mapFst)

import Control.Monad (liftM2, guard, mfilter)

{- $setup
>>> import qualified Solve
>>> import qualified Data.List as List
>>> import Data.Eq.HT (equating)
>>> import Control.Functor.HT (void)
>>> import Test.Utility
>>>    (solve, genOperands, genResult, genEquation,
>>>     normalizeSubExpr, normalizeSum)
>>> import qualified Test.QuickCheck as QC
-}


type NEList = NonEmpty.T []

{- |
The 'Ord' instance is needed for normalization in the tests
but does not refer to the value of the expression.
In order to prevent accidental use of '==' or '<'
we would have to implement those functions manually,
which is possible but a bit cumbersome.
-}
data SubExpr a = Number Integer | SubExpr a
   deriving (Eq, Ord, Show)
data Sum =
      Sum {positive :: NEList (SubExpr Product), negative :: [SubExpr Product]}
   deriving (Eq, Ord, Show)
data Product =
      Product {normal :: NEList (SubExpr Sum), reciprocal :: [SubExpr Sum]}
   deriving (Eq, Ord, Show)


split :: [a] -> [([a],[a])]
split =
   map (mapPair (map fst, map fst) . List.partition snd) .
   sequence . flip (ListHT.outerProduct (,)) [True, False]

neSplit :: NEList a -> [(NEList a, [a])]
neSplit (NonEmpty.Cons x xs) =
   map (mapFst (NonEmpty.Cons x)) $ split xs

splitMultiOrdered :: [a] -> [[NEList a]]
splitMultiOrdered [] = return []
splitMultiOrdered (x:xs) = do
   (ys,zs) <- neSplit (x!:xs)
   map (ys:) $ splitMultiOrdered zs

neSplitMultiOrdered :: [a] -> [(NEList (NEList a), [a])]
neSplitMultiOrdered xs = do
   (us,w,ws) <- ListHT.splitEverywhere xs
   (ys,zs) <- neSplit (w!:ws)
   map (mapPair (NonEmpty.cons ys, (us++))) $
      ([], zs) : map (mapFst NonEmpty.flatten) (neSplitMultiOrdered zs)


subExprsFromSet :: (NEList Integer -> [a]) -> NEList Integer -> [SubExpr a]
subExprsFromSet f xt@(NonEmpty.Cons x xs) =
   if null xs then [Number x] else map SubExpr $ f xt

topExprsFromSet :: [Integer] -> [SubExpr Sum]
topExprsFromSet xs = do
   y:ys <- fmap fst $ split xs
   flip subExprsFromSet (y!:ys) $ \yt ->
      exprsFromSet yt ++ map topProduct (exprsFromSet yt)

topProduct :: SubExpr Product -> Sum
topProduct zs = Sum (NonEmpty.singleton zs) []

anyExprsFromSet ::
   (Expression a) =>
   (NEList (SubExpr a) -> [SubExpr a] -> expr) ->
   NEList Integer -> [expr]
anyExprsFromSet cons xs = do
   (lhs,rs) <- neSplitMultiOrdered $ NonEmpty.flatten xs
   rhs <- splitMultiOrdered rs
   guard $ not $ null (NonEmpty.tail lhs) && null rhs
   liftM2 cons
      (traverse exprsFromSet lhs)
      (traverse exprsFromSet rhs)


class Expression expr where
   exprsFromSet :: NEList Integer -> [expr]
   format :: expr-> String
   eval :: expr -> Maybe Integer

instance (Expression a) => Expression (SubExpr a) where
   exprsFromSet = subExprsFromSet exprsFromSet
   format = formatSubExpr format
   eval (Number k) = Just k
   eval (SubExpr expr) = eval expr

instance Expression Sum where
   exprsFromSet = anyExprsFromSet Sum
   format (Sum pos neg) =
      let NonEmpty.Cons p ps = fmap format pos
          ns = map format neg
      in p ++ concatMap ("+"++) ps ++ concatMap ("-"++) ns
   eval (Sum pos neg) =
      mfilter (>=0) $
      liftM2 (-)
         (fmap NonEmpty.sum $ traverse eval pos)
         (fmap sum $ mapM eval neg)

instance Expression Product where
   exprsFromSet = anyExprsFromSet Product
   format (Product norm rec) =
      let NonEmpty.Cons n ns =
              fmap (formatSubExpr (addParen.format)) norm
          rs = map (formatSubExpr (addParen.format)) rec
      in n ++ concatMap ("*"++) ns ++ concatMap ("/"++) rs
   eval (Product norm rec) = do
      denom <- fmap product $ mapM eval rec
      guard $ denom/=0
      (q,r) <- fmap (flip divMod denom . NonEmpty.product) $ traverse eval norm
      guard $ r==0
      return q

formatSubExpr :: (a -> String) -> SubExpr a -> String
formatSubExpr _fmt (Number k) = show k
formatSubExpr fmt (SubExpr expr) = fmt expr

addParen :: String -> String
addParen str = "("++str++")"


{- |
>>> solve [25, 50, 75, 100, 3, 6] 952
25+6*75*(3+100)/50
(3*75*(6+100)-50)/25

>>> solve [75, 50, 2, 3, 8, 7] 812
50+(2+75)*(3+7)-8
2*7*(8+50)
...
50*(7+75)/(2+3)-8
(3+(2+75)/7)*(8+50)

>>> solve [100, 75, 50, 10, 5, 1] 102
1+100+5*10/50
1+100+50/5/10
...
(100+50/(75-5*10))/1
(100+(5+75)/(50-10))/1
(100+(75-5-50)/10)/1


prop> :{
   QC.forAll genOperands $ \xs ->
   QC.forAll (genResult xs) $ \x ->
      not $ null $ Solve.run (xs,x)
:}

prop> :{
   QC.forAll genOperands $ \xs ->
   QC.forAll (genResult xs) $ \x ->
   QC.forAll (QC.shuffle xs) $ \xs1 ->
      void (Solve.run (xs,x)) == void (Solve.run (xs1,x))
:}

prop> :{
   QC.forAll genOperands $ \xs ->
   QC.forAll (genResult xs) $ \x ->
   QC.forAll (QC.shuffle xs) $ \xs1 ->
      equating (List.sort . map (normalizeSubExpr normalizeSum))
         (Solve.run (xs,x)) (Solve.run (xs1,x))
:}

prop> :{
   QC.forAll genOperands $ \xs ->
   QC.forAll (genEquation xs) $ \(expr,x) ->
      elem expr $
      List.sort $ map (normalizeSubExpr normalizeSum) $ Solve.run (xs,x)
:}
-}
run :: ([Integer], Integer) -> [SubExpr Sum]
run (operands, result) =
   filter ((Just result ==) . Solve.eval) $ Solve.topExprsFromSet operands