練習に分数を循環小数で表現してみる。

分数を循環小数で表現しているページを見かけたので Haskell の練習に私もやってみました。

{-
> fractionToDecimal 1 2    --=> "0.5"
> fractionToDecimal 2 3    --=> "0.(6)"
> fractionToDecimal 2 7    --=> "0.(285714)"
> fractionToDecimal 1 66   --=> "0.0(15)"
> fractionToDecimal 1 666  --=> "0.0(015)"
> fractionToDecimal 12 123 --=> "0.(09756)"
> fractionToDecimal 1 333333333333333333333 --=> "0.(000000000000000000003)"
> fractionToDecimal 123 1234
 --=> "0.0(99675850891410048622366288492706645056726094003241491085899513776
33711507293354943273905)"
> fractionToDecimal 1234 12345 --=> "0.^C
Interrupted.
-}

listLen :: Int
listLen = 1000

fractionToDecimal :: Integer -> Integer -> String
fractionToDecimal x y = 
    if x > y 
         -- 循環部分の変換が目的なので分子が分母より大きい数は余りを分子としている
        then fractionToDecimal remainder y
        else if isCirculator x y
                then integerNUM ++ (tupleToString $ inTheMiddleOfList 0 decimalNUM)
                else case remainder of
                         0 -> show quotient
                         _ -> toString numList
            where
              (integerNUM,decimalNUM) = splitAt 2 $ toString $ take listLen $ --

              -- ["0","1","4","2","8","5","7","1","4"...] --=>"0.14285714..."
              toString ::[String] -> String
              toString ls = concat $ head ls:".":tail ls

              numList :: [String]
              numList = toList x y

              (quotient, remainder)= quotRem x y

-- 循環小数?
-- 循環小数でない場合は take 関数で取得する長さを変化させても結果は一緒。
isCirculator :: Integer -> Integer -> Bool
isCirculator x y = length (take listLen $ numList) /= length (take (listLen+1) $ numList)
  where
      numList = toList x y

-- Maybe (先頭部分, 循環する長さ, 残りの部分)を文字列に変換
tupleToString :: Maybe (String, Int, String) -> String
tupleToString tuple =
    case tuple of
        Nothing          -> "ERROR!" -- 分数は割りきれるか循環小数なのでERROR にはならないはず。
        Just (xs, n, ys) -> xs ++ "(" ++ fst (splitAt n ys) ++")"

-- 中間部分から(0 からサーチ)の循環を見つける。
-- Maybe (先頭部分, 循環する長さ, 残りの部分)
-- :循環数は残りの部分に含まれている。
inTheMiddleOfList :: Int -> String -> Maybe (String, Int, String)
inTheMiddleOfList p xs
    | p > length xs = Nothing
    | otherwise     = 
        case tJunkan $ snd splitAtp of  -- 分割して先頭からチェック
            Nothing  -> inTheMiddleOfList (p+1) xs
            Just len -> Just (fst splitAtp,len,snd splitAtp)
            where
                splitAtp = splitAt p xs

-- 先頭からの長さを1から増加させながら繰り返し部分の長さを見つける。
tJunkan :: String -> Maybe Int
tJunkan xs = junkan' xs 1

junkan' :: String -> Int -> Maybe Int
junkan' xs p | p > length xs = Nothing
             | otherwise     =
                  if isJunkan p xs 0
                      then Just p
                      else junkan' xs (p+1)

-- 先頭から指定の長さの部分がリスト全体に1回以上繰り返されている?
isJunkan :: Int -> String -> Int -> Bool
isJunkan l xs n
    | length sndXS < length fstXS && n > 0 = True  -- 残りの長さがチェック部分より短い
    | fstXS == fstXS2                      = isJunkan l sndXS2 (n+1)
    | otherwise                            = False -- 一致しない
        where
            (fstXS,  sndXS)  = splitAt l xs
            (fstXS2, sndXS2) = splitAt l sndXS

-- toList 1 2 --=> ["0","5"]
-- toList 1 3 --=> ["0","3","3","3"........]
-- toList 1 7 --=> ["0","1","4","2","8","5","7","1"......]
toList :: Integer -> Integer -> [String]
toList x y = [show quotient] ++
  if remainder == 0
      then []
      else toList (remainder * 10) y
    where
      (quotient, remainder)= quotRem x y

私の方がずっと長い・・・Orz