Day 25 was the Advent of Code tradition of a one-part problem, even though this had two parts. (And it wasn't until after I'd done this that I found the name for this kind of representation: balanced numbers, such as balanced ternary.)
The conversion from SNAFU numbers to decimal was just like any other base conversion: read a digit, multiply by the base, convert the rest.
readSnafu :: String -> Int readSnafu cs = foldl' go 0 cs where go acc c = acc * 5 + (snafuValue c) snafuValue :: Char -> Int snafuValue '2' = 2 snafuValue '1' = 1 snafuValue '0' = 0 snafuValue '-' = -1 snafuValue '=' = -2
The conversion from decimal to SNAFU took a bit more thinking. Eventually I worked out that, once you'd fixed the least significant few digits, those wouldn't change as more significant digits were added.
I took a long-winded approach of converting the number into a list of base-five elements (from least significant digit to most significant), then converting each digit in turn. If a digit was 3 or 4, I added a carry to the next digit.
showSnafu :: Int -> String showSnafu = packSnafu . toBase5R toBase5R :: Int -> [Int] toBase5R 0 =  toBase5R n = (r : (toBase5R k)) where (k, r) = n `divMod` 5 packSnafu :: [Int] -> String packSnafu digits | carry == 0 = shown | otherwise = (snafuRep carry) : shown where (carry, shown) = foldl' packSnafuDigit (0, "") digits packSnafuDigit :: (Int, String) -> Int -> (Int, String) packSnafuDigit (carry, acc) d | d' <= 2 = (0, (snafuRep d') : acc) | otherwise = (1, (snafuRep (d' - 5) : acc)) where d' = d + carry snafuRep :: Int -> Char snafuRep 2 = '2' snafuRep 1 = '1' snafuRep 0 = '0' snafuRep -1 = '-' snafuRep -2 = '=' snafuRep _ = error "Illegal number in show"
And that was it! All over for another year.