-- | -- Module : Advent.OCR -- Copyright : (c) Justin Le 2020 -- License : BSD3 -- -- Maintainer : justin@jle.im -- Stability : experimental -- Portability : non-portable -- -- Library to parse "ASCII Art" letters from puzzles. Compatible with all puzzles from 2015 to 2019. -- module Advent.OCR ( -- * Parse parseLetters , parseLettersWith , parseLettersEither , unsafeParseLetters , parseAsciiMap , asciiMapToLetters -- * Letter Map , LetterMap , defaultLetterMap -- ** Custom Letter Map , parseLetters' , parseLettersWith' , parseLettersEither' , unsafeParseLetters' , asciiMapToLetters' ) where import Advent.OCR.Internal import Data.Bifunctor import Data.Maybe import Data.Set (Set) import qualified Data.Set as S -- | A version of 'parseLettersWith'' accepting a custom 'LetterMap' -- letterform database. parseLettersWith' :: LetterMap -- ^ database of letterforms -> (a -> Int) -- ^ get X -> (a -> Int) -- ^ get Y -> Set a -> Maybe String parseLettersWith' lm f g = parseLettersV2 lm . S.map (\x -> V2 (f x) (g x)) -- | A version of 'parseLetters' that takes a set of any type of value, as -- long as you provide functions to access the X and Y coordinates. parseLettersWith :: (a -> Int) -- ^ get X -> (a -> Int) -- ^ get Y -> Set a -> Maybe String parseLettersWith = parseLettersWith' defaultLetterMap -- | A version of 'parseLetters'' accepting a custom 'LetterMap' letterform -- database. parseLetters' :: LetterMap -- ^ database of letterforms -> Set (Int, Int) -- ^ set of points -> Maybe String -- ^ result, with unknown letters replaced with "?" parseLetters' lm = parseLettersV2 lm . S.mapMonotonic (uncurry V2) -- | The go-to default: given a set of point coordinates, parse it into -- the string it represents. Should be compatible with any Advent of Code -- challenge from 2015 to 2019. -- -- @ -- 'parseLetters' 'defaultLetterMap' myPoints -- -- -- or, using Data.Default -- 'parseLetters' 'Data.Default.Class.def' myPoints -- @ -- -- A 'Nothing' means that there were no recognized letters found. A 'Just' -- means that least 50% of letter forms are recognized. Unrecognized -- characters will be replaced with "?"; for more information, use -- 'parseLettersEither'. -- -- This function is robust to changes in orientation or flipping, but will -- be fastest if the coordinates are oriented with (0,0) on the upper left -- corner. However, because of this, it might return the wrong answer if -- your coordinates are /not/ oriented this way and your result is -- symmetrical: it'll always prioritize the interpretaion against (0,0) -- upper-left orientation first. parseLetters :: Set (Int, Int) -- ^ set of points -> Maybe String -- ^ result, with unknown letters replaced with "?" parseLetters = parseLetters' defaultLetterMap -- | A version of 'parseLettersEither'' accepting a custom 'LetterMap' -- letterform database. parseLettersEither' :: LetterMap -- ^ database of letterforms -> Set (Int, Int) -> Maybe [Either (Set (Int, Int)) Char] parseLettersEither' lm = (fmap . map . first . S.mapMonotonic) (\(V2 x y) -> (x, y)) . parseLettersEitherV2 lm . S.mapMonotonic (uncurry V2) -- | A version of 'parseLetters' returning a list of characters that were -- either recognized or unrecognized; in the case of unrecognized -- characters, returns the set of their coordinates but shifted to (0, 0) -- as its upper left corner. parseLettersEither :: Set (Int, Int) -> Maybe [Either (Set (Int, Int)) Char] parseLettersEither = parseLettersEither' defaultLetterMap -- | A version of 'unsafeParseLetters'' accepting a custom 'LetterMap' -- letterform database. unsafeParseLetters' :: LetterMap -> Set (Int, Int) -> String unsafeParseLetters' lm = fromMaybe (error "Advent.OCR.unsafeParseLetters': Unable to parse letters") . parseLetters' lm -- | A version of 'parseLetters' that will be undefined ('error') when no -- parse is found. unsafeParseLetters :: Set (Int, Int) -> String unsafeParseLetters = fromMaybe (error "Advent.OCR.unsafeParseLetters: Unable to parse letters") . parseLetters -- | Parse raw ASCII art into a set of points, usable with -- 'parseLetters'. parseAsciiMap :: Set Char -- ^ characters to use as "on"/included -> String -- ^ raw ASCII art -> Set (Int, Int) parseAsciiMap c = S.mapMonotonic (\(V2 x y) -> (x, y)) . parseAsciiMapV2 c -- | A version of 'asciiMapToLetters'' accepting a custom 'LetterMap' -- letterform database. asciiMapToLetters' :: Set Char -- ^ characters to use as "on"/included in ASCII art -> LetterMap -- ^ database of letterforms -> String -- ^ raw ASCII art -> Maybe String asciiMapToLetters' c lm = parseLettersV2 lm . parseAsciiMapV2 c -- | Convenient all-in-one utility function combining 'parseAsciiMap' and -- 'parseLetters', to directly parse ASCII art into its letters. asciiMapToLetters :: Set Char -- ^ characters to use as "on"/included in ASCII art -> String -- ^ raw ASCII art -> Maybe String asciiMapToLetters c = asciiMapToLetters' c defaultLetterMap