{-# LANGUAGE LambdaCase #-}

module Euler.Util.Digit
  (
  -- $setup

  -- * Conversions to list of digits
    digits, textDigits, stringDigits

  -- * Conversions to single digits
  , charIntMaybe

  -- * Conversions from digits to integers
  , textIntMaybe, unDigits

  -- * Other stuff
  , intPalindrome
  ) where

import Euler.Prelude

import qualified Data.List as List
import qualified Data.Text.Read as TextRead

{- $setup

>>> import Test.QuickCheck

-}

{- |

Digits of a positive integer.

-}
digits :: (Integral a, Integral b)
  => b   -- ^ Base
  -> a   -- ^ Number to convert
  -> [b]
digits base = digitsRev base >>> List.reverse

{- |

Digits of a positive integer as a list, in reverse order. This is slightly more
efficient than in forward order.

-}
digitsRev :: (Integral a, Integral b)
  => b   -- ^ Base
  -> a   -- ^ Number to convert
  -> [b]
digitsRev _ 0 = []
digitsRev base i = let (rest, lastDigit) = quotRem i (fromIntegral base)
                   in  fromIntegral lastDigit : digitsRev base rest

{- |

The positive integer represented by a list of digits,

-}
unDigits :: (Integral a, Integral b)
  => b   -- ^ Base
  -> [b] -- ^ Digits
  -> a
unDigits base = foldl' f 0
  where f a b = a * fromIntegral base + fromIntegral b

{- |

Get the values of numeric characters from a string, ignoring any non-numeric
characters.

>>> stringDigits " 0 x0 42a"
[0,0,4,2]

-}
stringDigits :: String -> [Int]
stringDigits = mapMaybe charIntMaybe

{- |

Same as 'stringDigits', for 'Text' instead of 'String'.

-}
textDigits :: Text -> [Int]
textDigits = stringDigits . unpack

textIntMaybe  :: Integral a => Text -> Maybe a
textIntMaybe =
  TextRead.decimal <&>
  \case
    Right (a, _) -> Just a
    _            -> Nothing

{- |

Maps the ten numeric ascii characters to their numeric values, and all other
characters to 'Nothing'.

>>> charIntMaybe '0'
Just 0

>>> charIntMaybe '9'
Just 9

>>> charIntMaybe 'a'
Nothing

-}
charIntMaybe :: Char -> Maybe Int
charIntMaybe c =
  guard (List.elem c ['0'..'9']) $> digitToInt c

{- |

@'intPalindrome' b n@ indicates whether the base-/b/ representation /n/ is a
palindrome.

>>> intPalindrome 10 <$> [33, 13431, 21, 335]
[True,True,False,False]

Zero and one are palindromes in any base.

prop> \(Positive n) -> intPalindrome (n+1) 0
prop> \(Positive n) -> intPalindrome (n+1) 1

-}
intPalindrome :: (Integral a, Integral b)
  => b    -- ^ Base
  -> a    -- ^ Number to test
  -> Bool
intPalindrome b n =
  let ds = digitsRev b n
  in  ds == List.reverse ds