Search code examples
haskellmodular-arithmetic

Excel column to Int and vice-versa - improvements sought


In the code below, I am happy with the functionality i.e. the code produces the output that I expect. However, comparing the length of toCol to toInt - I am interested in knowing if you could offer something to trim it (i.e. toCol ) down. Many thanks!

-- given a spreadsheet column as a string
-- returns integer giving the position of the column
-- ex:
-- toInt "A" = 1
-- toInt "XFD" = 16384
toInt :: String -> Int
toInt = foldl fn 0
  where
    fn = \a c -> 26*a + ((ord c)-64)

-- given a integer returns
-- the column to be found at that position as a [Char]
-- ex:
-- toCol 1 = "A"
-- toCol 16384 = "XFD"
toCol :: Int -> [Char]
toCol n = toCol' n []
  where
    toCol' 0 a = a
    toCol' n a =
      let r = mod n 26 in
        case (r == 0) of
          True -> toCol' (div (n-1) 26) ('Z':a)
          False -> toCol' (div n 26) (chr(r + 64) : a)

Solution

  • Whenever you are building up a finite list recursively, think about unfoldr :: (b -> Maybe (a, b)) -> b -> [a] from Data.List (although because it unfolds from the wrong direction, we end up needing to reverse the list too). The syntax extension MultiWayIf also helps make things nicer.

    {-# LANGUAGE MultiWayIf #-}
    
    toCol :: Int -> [Char]
    toCol = reverse . unfoldr (\n -> let r = n `mod` 26 in
              if | n == 0    -> Nothing
                 | r == 0    -> Just ('Z'         , n-1 `div` 26)
                 | otherwise -> Just (chr (r + 64), n   `div` 26))
    

    Note that this also makes toCol point-free. If you would prefer to not have an extension enabled and prefer to pattern match, you can also do that:

    toCol :: Int -> [Char]
    toCol = reverse . unfoldr (\n -> case (n, n `mod` 26) of 
                                       (0, _) -> Nothing
                                       (n, 0) -> Just ('Z'         , n-1 `div` 26)
                                       (n, r) -> Just (chr (r + 64), n   `div` 26))