packages feed

ca-patterns-0.2.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.
-}

{-# OPTIONS_GHC -fno-warn-unused-imports #-}
{-# LANGUAGE BlockArguments, LambdaCase, OverloadedStrings, FlexibleContexts #-}

module Text.RLE (Rule, parse, parseMany, 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 Data.Text(Text)

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 (flip 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 (many 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 False rows
      Alive -> add True 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.

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)

{-|
Parse zero or more RLE files. The argument can be 'String', 'Text', or
any other type with a 'Stream' instance.
-}
parseMany :: (Stream s Identity Char) => s -> Maybe [(Maybe Rule, Pattern)]
parseMany str =
  case runParser (many parseRLE) () "" str of
    Left _ -> Nothing
    Right rles -> Just (map toPattern rles)

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)
  ((False : row) : rows, runs) -> updateRuns (row : rows) (add Dead runs)
  ((True : 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)