ca-patterns-0.1.0.0: src/Text/RLE.hs
{-|
The RLE (run length encoded) format is a common way of representing
patterns in life-like cellular automata. RLE files consist of three
sections:
1. Zero or more comment lines beginning with @#@
2. A header of the form @x = [width], y = [height], rule = [rule]@.
@[width]@ and @[height]@ are natural numbers, and @[rule]@ is the
rule the pattern is meant to be run in, such as @B36/S23@. The "rule" field
is optional.
3. The content of the pattern. @b@ represents a dead cell, @o@ represents
a live cell, and @$@ denotes the end of a row. A run of identical
characters can be abbreviated with a number, e.g. @4o@ is short for
@oooo@ (hence the name "run length encoded"). This section must be
terminated by a @!@ character.
A glider in the Game of Life could be represented like so:
> #N glider
> x = 3, y = 3, rule = B3/S23
> 3o$2bo$bo!
See this [LifeWiki article](https://conwaylife.com/wiki/Run_Length_Encoded)
for more information.
-}
{-# LANGUAGE BlockArguments, LambdaCase, OverloadedStrings, FlexibleContexts #-}
module Text.RLE (Rule, parse, make, printAll) where
import qualified Data.Char as Char
import qualified Data.String as String
import qualified Control.Applicative as Ap
import Data.Functor.Identity (Identity)
import Data.String (IsString)
import Text.Parsec
( Parsec, Stream, runParser
, (<|>), try, many, many1, manyTill, satisfy
, anyChar, digit, string, spaces
)
import qualified Data.Text.IO as IO
import qualified Data.CA.Pattern as Pat
import Data.CA.Pattern (Pattern)
{-|
A string representing a cellular automaton, such as @"B36/S23"@ or
@"B3/S2-i34q"@.
-}
type Rule = String
data RunType = Dead | Alive | Newline
deriving (Eq)
type Run = (Int, RunType)
data Header = Header Int Int (Maybe Rule)
type RLEData = (Header, [Run])
type Parser s = Parsec s ()
natural :: (Stream s Identity Char) => Parser s Int
natural = do
num <- many1 digit
spaces
return (read num)
symbol :: (Stream s Identity Char) => String -> Parser s ()
symbol sym = string sym >> spaces
parseWidth :: (Stream s Identity Char) => Parser s Int
parseWidth = symbol "x" >> symbol "=" >> natural
parseHeight :: (Stream s Identity Char) => Parser s Int
parseHeight = symbol "," >> symbol "y" >> symbol "=" >> natural
parseRule :: (Stream s Identity Char) => Parser s Rule
parseRule = do
symbol "," >> symbol "rule" >> symbol "="
rule <- many1 (satisfy (not . Char.isSpace))
spaces
return rule
parseHeader :: (Stream s Identity Char) => Parser s Header
parseHeader = Ap.liftA3 Header parseWidth parseHeight maybeRule
where maybeRule = fmap Just parseRule <|> return Nothing
parseRunType :: (Stream s Identity Char) => Parser s RunType
parseRunType = (symbol "b" >> return Dead)
<|> (symbol "o" >> return Alive)
<|> (symbol "$" >> return Newline)
parseRun :: (Stream s Identity Char) => Parser s Run
parseRun = Ap.liftA2 (,) (natural <|> return 1) parseRunType
parseData :: (Stream s Identity Char) => Parser s RLEData
parseData = do
rle <- Ap.liftA2 (,) parseHeader (many1 parseRun)
symbol "!"
return rle
parseComment :: (Stream s Identity Char) => Parser s ()
parseComment = symbol "#" >> manyTill anyChar newline >> return ()
where newline = try (symbol "\n")
parseRLE :: (Stream s Identity Char) => Parser s RLEData
parseRLE = spaces >> many parseComment >> parseData
updateRows :: [Run] -> [[Pat.Cell]] -> [[Pat.Cell]]
updateRows = curry \case
([], rows) -> rows
((0, _) : runs, rows) -> updateRows runs rows
((n, rt) : runs, rows) -> let
add cell = \case
[] -> [[cell]]
(row : rest) -> (cell : row) : rest
runs' = (n - 1, rt) : runs
rows' = case rt of
Dead -> add Pat.Dead rows
Alive -> add Pat.Alive rows
Newline -> [] : rows
in updateRows runs' rows'
toPattern :: RLEData -> (Maybe Rule, Pattern)
toPattern (Header h w rule, runs) = let
rows = updateRows (reverse runs) []
pat = Pat.setDimensions h w (Pat.fromList rows)
in (rule, pat)
{-|
Parse an RLE file, returning a 'Rule' (if it exists) and a 'Pattern'.
The argument can be 'String', 'Text', or any other type with a 'Stream'
instance.
This parser is fairly liberal. Whitespace is allowed everywhere except
in the middle of a number or rulestring, and there need not be a
newline after the header. Also, text after the final @!@ character is
ignored.
-}
parse :: (Stream s Identity Char) => s -> Maybe (Maybe Rule, Pattern)
parse str =
case runParser parseRLE () "" str of
Left _ -> Nothing
Right rle -> Just (toPattern rle)
updateRuns :: [[Pat.Cell]] -> [Run] -> [Run]
updateRuns = let
add rt = \case
(n, rt') : runs | rt == rt' -> (n + 1, rt) : runs
runs -> (1, rt) : runs
in curry \case
([], runs) -> reverse runs
([] : rows, runs) -> updateRuns rows (add Newline runs)
((Pat.Dead : row) : rows, runs) -> updateRuns (row : rows) (add Dead runs)
((Pat.Alive : row) : rows, runs) -> updateRuns (row : rows) (add Alive runs)
fromShow :: (Show a, IsString s) => a -> s
fromShow = String.fromString . show
showRunType :: (IsString s) => RunType -> s
showRunType = \case
Dead -> "b"
Alive -> "o"
Newline -> "$"
showRun :: (Semigroup s, IsString s) => Run -> s
showRun = \case
(1, rt) -> showRunType rt
(n, rt) -> fromShow n <> showRunType rt
showRuns :: (Semigroup s, IsString s) => Int -> [Run] -> s
showRuns = curry \case
(_, []) -> ""
(0, runs) -> "\n" <> showRuns 30 runs
(n, run : runs) -> showRun run <> showRuns (n - 1) runs
{-|
Convert a 'Pattern' into an RLE. If the first argument is 'Nothing',
the generated RLE will have no "rule" field in its header.
-}
make :: (Semigroup s, IsString s) => Maybe Rule -> Pattern -> s
make rule pat = let
x = "x = " <> fromShow (Pat.width pat)
y = ", y = " <> fromShow (Pat.height pat)
r = case rule of
Nothing -> ""
Just str -> ", rule = " <> String.fromString str
runs = updateRuns (Pat.toList pat) []
in x <> y <> r <> "\n" <> showRuns 30 runs <> "!\n"
{-|
Convert a list of patterns into RLEs and print them. Example:
> import qualified Text.RLE as RLE
> import Data.CA.List (withDimensions)
> main = RLE.printAll (Just "B3/S23") (withDimensions 4 4)
The program above will print the RLE of every possible pattern
contained in a 4 by 4 square. This data can then be piped into another
program such as apgsearch.
-}
printAll :: Maybe Rule -> [Pattern] -> IO ()
printAll rule = mapM_ (IO.putStrLn . make rule)