m0meni
m0meni

Reputation: 16435

Can't match tuple inside of foldl

I have the following code, which should™ convert an excel column type its corresponding number. For example AA into 27 and AB into 28:

import Data.Char (ord)
import Data.List (foldl1')

columnToNumber :: String -> Int
columnToNumber s = foldl1' (\acc (i, v) -> acc + 26 ^ i * v) (values s)
  where values s = zip (reverse [0..(length s)]) ((\c -> ord c - 64) <$> s)

The idea is to take the string "AA" convert it to the corresponding numbers

["A", "A"] -> [1, 1]

and zip it with the base so from right to left 26^0, 26^1, 26^2, and so on.

zip [1, 0] [1, 1] -> [(1, 1), (0, 1)]

That way the result of the fold would be

26^1 * 1 + 26^0 * 1 = 27

Unfortunately, I'm getting the following errors and I'm not sure why:

ExcelSheetColumn.hs:7:34:
    Couldn't match expected type ‘Int’
                with actual type ‘(Integer, Int)’
    In the pattern: (i, v)
    In the first argument of ‘foldl1'’, namely
      ‘(\ acc (i, v) -> acc + 26 ^ i * v)’
    In the expression:
      foldl1' (\ acc (i, v) -> acc + 26 ^ i * v) (values s)

ExcelSheetColumn.hs:7:63:
    Couldn't match type ‘(Int, Int)’ with ‘Int’
    Expected type: [Int]
      Actual type: [(Int, Int)]
    In the second argument of ‘foldl1'’, namely ‘(values s)’
    In the expression:
      foldl1' (\ acc (i, v) -> acc + 26 ^ i * v) (values s)

Could someone help me out?

Upvotes: 1

Views: 195

Answers (2)

Random Dev
Random Dev

Reputation: 52280

to get it compiling you actually just have to switch foldl1' to foldl' and add the starting accumulator:

import Data.Char (ord)
import Data.List (foldl')

columnToNumber :: String -> Int
columnToNumber s = foldl' (\acc (i, v) -> acc + 26 ^ i * v) 0 (values s)
  where values s = zip (reverse [0..(length s)]) ((\c -> ord c - 64) <$> s)

if you add the suggestion Free_D made (start at length s - 1):

columnToNumber :: String -> Int
columnToNumber s = foldl' (\acc (i, v) -> acc + 26 ^ i * v) 0 (values s)
  where values s = zip (reverse [0..(length s -1)]) ((\c -> ord c - 64) <$> s)

you get the desired results:

λ> columnToNumber "AA"
27
λ> columnToNumber "AB"
28

I don't know if you actually gonna need this but hey why not:

what you probably don't like is that columnToNumber "A1" is 11 - to fix this you need to tread digits differently from letters:

columnToNumber :: String -> Int
columnToNumber s = foldl' (\acc (i, v) -> acc + 26 ^ i * v) 0 (values s)
  where values s = zip (reverse [0..(length s -1)]) (parse <$> s)
        parse c
          | c >= '0' && c <= '9' = ord c - ord '0'
          | otherwise = ord c - 64

Upvotes: 6

smac89
smac89

Reputation: 43128

Looking at the definition of foldl1', it has to take two things that are of the same type and produce something similar

*Main Data.List> :t foldl1'
foldl1' :: (a -> a -> a) -> [a] -> a

But foldl is what you want:

*Main Data.List> :t foldl
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b

So essentially this:

import Data.Char (ord, toUpper)

columnToNumber :: String -> Int
columnToNumber s = foldl (\acc (i, v) -> acc + 26 ^ i * v) 0 $ values s where 
    values s = zip [l - 1, l - 2 ..0] ((\c -> (ord.toUpper) c - 64) <$> s) where
        l = length s

Upvotes: 2

Related Questions