Why does the type of max prod nextProd get inferred as Int->Int->Int instead of just Int in the below function?
import qualified Data.Vector as DV
largestProduct::DV.Vector Int->(Int, Int)
largestProduct digits = foldl lastProdAndMax (0,0) [1..((DV.length digits) - 13)] where
lastProdAndMax _ 1 = (first, first) where first = DV.foldl (*) 1 $ DV.take 13 digits
lastProdAndMax (prod, max) i = (nextProd, max prod nextProd)
where nextProd = (prod `div` ((DV.!) digits (i-13))) * ((DV.!) digits i)
Your error is in
lastProdAndMax (prod, max) i = (nextProd, max prod nextProd)
^^^
in that scope, max is a Int (overloading the max function).
Related
Haskell Code Problem
Description: Code is supposed to return how many digits in the number divide the number as a whole.
For example, 12 has two digits [1, 2], both of which divide 2 (12%2 and 12%1 are both 0) so 2 is returned as there are two digits that divide the number.
For 102, 2 is returned as 1 and 2 both divide 102, division by 0 is undefined.
However, with this code I get errors with numbers containing 0s in the middle of the number (e.g. 1001020)
I get "Program Error: Prelude.read: no parse"
Any help will be greatly appreciated. Many thanks.
import Control.Monad
import Data.Array
import Data.Bits
import Data.Char
import Data.List
import Data.Set
import Debug.Trace
import System.Environment
import System.IO
import System.IO.Unsafe
findDigits :: Int -> Int
findDigits n = digits n n 0 (lengths n)
where
digits n on count endCheck
| endCheck == 0 = count
| header n == 0 = digits (tailer n) on count (endCheck-1)
| on `mod` header n == 0 = digits (tailer n) on (count+1) (endCheck-1)
| otherwise = digits (tailer n) on count (endCheck-1)
header :: Int -> Int
header x = digitToInt . head . show $ x
tailer :: Int -> Int
tailer x = read . tail . show $ x
lengths :: Int -> Int
lengths x = length . show $ x
I think you are trying to do too much in a function. Uually it is better to work with small functions that each solve a simple task, and then combine these in functions that are small as well, and perform a (slightly) more sophisticated task.
For example we can make a function digits :: Int -> [Int] that returns a list of digits:
digits :: Int -> [Int]
digits x | x >= 10 = r : digits q
| otherwise = [x]
where (q,r) = quotRem x 10
For example:
Prelude> digits 102
[2,0,1]
We can then filter these digits to check that the digits are not zero (since then it is not dividable), and that the number is dividable by that digit:
dividableDigits :: Int -> [Int]
dividableDigits n = filter (\x -> x /= 0 && mod n x == 0) (digits n)
Now it is a matter of counting the numbers that match. I leave that as an exercise.
I use recursion to compare two numbers(ex 123 is the same with 123) and store how many digits they have in common(ex 123 compared to 123 has 3 and with 124 has 2).
While my program does find that number i want to put conditions for every situation (ex if they have 2 digits in common output value 44 or if they have 3 do something else etc.) but no comparison is done. Can someone please explain to me what and why that happens. Here my code :
dg :: Int->Int->Int
dg 0 0 = 0
dg x y = if (c==2) then 23 else 24 -- c = common digits
where c = digits (x `div` 10) (y `div` 10) + if (x `mod` 10 == y `mod` 10) then 1 else 0
I run hugs deleting the "if" condition, giving input 10 10 the output is 2 but when i leave it on and rerun it, it goes to 24 (while 23 is correct). I'm really confused.
To find how many digits two Int have in common, using the string representation isn't too bad (It's actually terrible, see update below).
commonDigits :: Int -> Int -> Int
commonDigits a b = length . filter id $ zipWith (==) (reverse $ show a) (reverse $ show b)
reverse is needed to make sure the digits align properly.
If you're into point-free style:
import Data.Function
commonDigits :: Int -> Int -> Int
commonDigits = fmap (length . filter id) . zipWith (==) `on` reverse . show
Then if you want to return a special Int depending on the result of commonDigits you can use a separate function:
specialResult :: Int -> Int -> Int
specialResult a b =
case commonDigits a b of
2 -> 23
3 -> 48
_ -> 256
Update: For negative integers this method is not good. If only one of the argument is negative this function should behave like your mod 10 version. If both are negative there is the possibility that the '-' sign is counted as a digit.
Let's use a digits function that will give a list of digits for both positive and negative numbers from the least significant to the most significant digits. What happens if the number 0 is given as input, should we return an empty list or [0]? Your call on that one, but I'm going to assume you want [0].
digits :: Int -> [Int]
digits 0 = [0]
digits n = digits' (abs n)
where
digits' 0 = []
digits' n = n `mod` 10 : digits' (n `div` 10)
With this new digits function we can rewrite commonDigits to:
commonDigits :: Int -> Int -> Int
commonDigits = fmap (length . filter id) . zipWith (==) `on` digits
I am working on a program to get the closest prime number by the exponent of 2, this is between an interval.
module Main where
import Data.Char
import System.IO
import Control.Monad (liftM)
data PGetal = G Bool | P Int
instance Show PGetal where
show (P n) = show n
show (G False) = "GEEN PRIEMGETAL GEVONDEN"
mPriem::(Int, Int) -> PGetal
mPriem (x,y) | (x > y) = G False
| (x > 1000000) = G False
| (y > 1000000) = G False
| (null (getAllPriem(x,y))) = G False
| otherwise = P (kleinsteVerschilF(getAllPriem(x,y),1000000,1))
kleinsteVerschilF:: ([Int], Int , Int) -> Int
kleinsteVerschilF ([],_, priemGetal) = priemGetal
kleinsteVerschilF (priem1:priemcss, kleinsteVerschil,priemGetal)=
if(kleinsteVerschil <= kleinsteVerschilMetLijst (priem1,(getMachtenVanTwee(0)),1000000))then kleinsteVerschilF(priemcss, kleinsteVerschil,priemGetal)
else kleinsteVerschilF (priemcss,kleinsteVerschilMetLijst(priem1,(getMachtenVanTwee(0)),1000000), priem1)
kleinsteVerschilMetLijst :: (Int,[Int],Int) -> Int
kleinsteVerschilMetLijst ( _,[],kleinsteVerschil) = kleinsteVerschil
kleinsteVerschilMetLijst (x,tweeMachten1:tweeMachtencss,kleinsteverschil)=
if((abs(x-tweeMachten1)) < kleinsteverschil)
then kleinsteVerschilMetLijst(x,tweeMachtencss, (abs(x-tweeMachten1)))
else kleinsteVerschilMetLijst(x,tweeMachtencss, kleinsteverschil)
getAllPriem :: (Int, Int) ->[Int]
getAllPriem (x,y) = filter isPriem [x..y]
getMachtenVanTwee ::(Int) -> [Int]
getMachtenVanTwee (macht)
|(functieMachtTwee(macht)< 1000000) = (functieMachtTwee(macht)) : (getMachtenVanTwee ((macht+1)))
| otherwise = []
functieMachtTwee:: (Int) -> Int
functieMachtTwee (x) = 2^x
isPriem n = (aantalDelers n)==2
aantalDelers n = telAantalDelersVanaf n 1
telAantalDelersVanaf n kandidaatDeler
| n == kandidaatDeler = 1
| mod n kandidaatDeler == 0
= 1 + telAantalDelersVanaf n (kandidaatDeler+1)
| otherwise
= telAantalDelersVanaf n (kandidaatDeler+1)
aantalDelers2 getal = telDelers getal 1 0
where telDelers n kandidaat teller
| n == kandidaat = 1+teller
| mod n kandidaat == 0
= telDelers n (kandidaat+1) (teller+1)
| otherwise
= telDelers n (kandidaat+1) teller
transform :: [String] -> [PGetal]
transform [] = []
transform (cs:css) =
let (a : b: _ ) = words cs
in (mPriem ((read(a)),(read(b))): transform css)
main :: IO ()
main = do
n <- read `liftM` getLine :: IO Int
lss <- lines `liftM` getContents
let cases = take n lss
let vs = (transform (lss))
putStr $ unlines $ map show vs
When I use the mPriem function, it works fine.
But it needs to work with an input txt file, so I made a .exe file with the ghc command. I also added this .txt file in the folder.
10
1 1
1 3
1 100
200 250
14 16
5 10
20 31
16 50
100 120
5200 7341
When I use in command line this command, it does nothing. There is no output. I can't CTRL+C to stop the program, so I think it crashes. But I don't know what's wrong.
type invoer.txt | programma.exe
Your program works, but is not that efficient and personally I find it not that elegant (sorry :S) because you introduce a lot of "noise". As a result it takes a lot of time before output is written.
If I understand the problem statement correctly, each line (except the first), contains two integers, and you need to count the amount of prime numbers between these two numbers (bounds inclusive?)
First of all, you can do this more elegantly by defining a function: cPrime :: Int -> Int -> Int that takes as input the two numbers and returns the amount of prime numbers:
cPrime :: Int -> Int -> Int
cPrime a b = count $ filter isPrime [a .. b]
You can improve performance by improving your prime checking algorithm. First of all, you do not need to check whether 1 is a divisor, since 1 is always a divisor. Furthermore, you can prove mathematically that there is no divisor greater than sqrt(n) (except for n) that divides n; unless there is another divider that is smaller than sqrt(n). So that means that you can simply enumerate all numbers between 2 and sqrt n and from the moment one of these is a divisor, you can stop: you have proven the number is not prime:
isPrime :: Int -> Bool
isPrime 1 = False
isPrime 2 = True
isPrime n = all ((0 /=) . mod n) (2:[3,5..m])
where m = floor $ sqrt $ fromIntegral n
Now I'm not sure what you aim to do with kleinsteVerschilF.
I want to reverse an Integer in Haskell with recursion. I have a small issue.
Here is the code :
reverseInt :: Integer -> Integer
reverseInt n
| n>0 = (mod n 10)*10 + reverseInt(div n 10)
| otherwise = 0
Example 345
I use as input 345 and I want to output 543
In my program it will do....
reverseInt 345
345>0
mod 345 10 -> 5
reverseInt 34
34
34>0
mod 34 10 -> 4
reverseInt 3
3>0
mod 3 10 -> 3
reverseInt 0
0=0 (ends)
And at the end it returns the sum of them... 5+4+3 = 12.
So I want each time before it sums them, to multiple the sum * 10. So it will go...
5
5*10 + 4
54*10 + 3
543
Here's a relatively simple one:
reverseInt :: Int -> Int
reverseInt 0 = 0
reverseInt n = firstDigit + 10 * (reverseInt $ n - firstDigit * 10^place)
where
n' = fromIntegral n
place = (floor . logBase 10) n'
firstDigit = n `div` 10^place
Basically,
You take the logBase 10 of your input integer, to give you in what place it is (10s, 100s, 1000s...)
Because the previous calculation gives you a floating point number, of which we do not need the decimals, we use the floor function to truncate everything after the decimal.
We determine the first digit of the number by doing n 'div' 10^place. For example, if we had 543, we'd find place to be 2, so firstDigit = 543/100 = 5 (integer division)
We use this value, and add it to 10 * the reverse of the 'rest' of the integer, in this case, 43.
Edit: Perhaps an even more concise and understandable version might be:
reverseInt :: Int -> Int
reverseInt 0 = 0
reverseInt n = mod n 10 * 10^place + reverseInt (div n 10)
where
n' = fromIntegral n
place = (floor . logBase 10) n'
This time, instead of recursing through the first digit, we're recursing through the last one and using place to give it the right number of zeroes.
reverseInt :: Integer -> Integer
reverseInt n = snd $ rev n
where
rev x
| x>0 = let (a,b) = rev(div x 10)
in ((a*10), (mod x 10)*a + b)
| otherwise = (1,0)
Explanation left to reader :)
I don't know convenient way to found how many times you should multiply (mod n 10) on 10 in your 3rd line. I like solution with unfoldr more:
import Data.List
listify = unfoldr (\ x -> case x of
_ | x <= 0 -> Nothing
_ -> Just(mod x 10, div x 10) )
reverse_n n = foldl (\ acc x -> acc*10+x) 0 (listify n)
In listify function we generate list of numbers from integer in reverse order and after that we build result simple folding a list.
Or just convert it to a string, reverse it and convert it back to an integer:
reverseInt :: Integer -> Integer
reverseInt = read . reverse . show
More (not necessarily recursion based) answers for great good!
reverseInt 0 = 0
reverseInt x = foldl (\x y -> 10*x + y) 0 $ numToList x
where
numToList x = if x == 0 then [] else (x `rem` 10) : numToList (x `div` 10)
This is basically the concatenation of two functions : numToList (convert a given integer to a list 123 -> [1,2,3]) and listToNum (do the opposite).
The numToList function works by repeatedly getting the lowest unit of the number (using rem, Haskell's remainder function), and then chops it off (using div, Haskell's integer division function). Once the number is 0, the empty list is returned and the result concatenates into the final list. Keep in mind that this list is in reverse order!
The listToNum function (not seen) is quite a sexy piece of code:
foldl (\x y -> 10*x + y) 0 xs
This starts from the left and moves to the right, multiplying the current value at each step by 10 and then adding the next number to it.
I know the answer has already been given, but it's always nice to see alternative solutions :)
The first function is recursive to convert the integer to a list. It was originally reversing but the re-conversion function reversed easier so I took it out of the first. The functions can be run separately. The first outputs a tuple pair. The second takes a tuple pair. The second is not recursive nor did it need to be.
di 0 ls = (ls,sum ls); di n ls = di nn $ d:ls where (nn,d) = divMod n 10
di 3456789 []
([3,4,5,6,7,8,9],42)
rec (ls,n) = (sum [y*(10^x)|(x,y) <- zip [0..] ls ],n)
Run both as
rec $ di 3456789 []
(9876543,42)
I'm trying to solve Project Euler problem #92 with Haskell. I started learning Haskell recently. It's the first Project Euler problem I've tried to solve with Haskell, but my piece of code doesn't terminate even in 10 minutes. I know you don't give me the answer directly, but again I should warn I find answer with c++ doesn't give answer of Euler or new logic to solve Euler. I'm just curious why that guy doesn't work fast and what should I do to make it faster?
{--EULER 92--}
import Data.List
myFirstFunction 1 = 0
myFirstFunction 89 = 1
myFirstFunction x= myFirstFunction (giveResult x)
giveResult 0 = 0
giveResult x = (square (mod x 10)) + (giveResult (div x 10))
square x = x*x
a=[1..10000000]
main = putStrLn(show (sum (map myFirstFunction a)))
The biggest speedup can of course be gained from using a better algorithm. I'm not going deep into that here, though.
Original algorithm tweakings
So let's focus on improving the used algorithm without really changing it.
You never give any type signature, therefore the type defaults to the arbitrary precision Integer. Everything here fits easily in an Int, there's no danger of overflow, so let's use that. Adding a type signature myFirstFunction :: Int -> Int helps: time drops from Total time 13.77s ( 13.79s elapsed) to Total time 6.24s ( 6.24s elapsed) and total allocation drops by a factor of about 15. Not bad for such a simple change.
You use div and mod. These always compute a non-negative remainder and the corresponding quotient, so they need some extra checks in case some negative numbers are involved. The functions quot and rem map to the machine division instructions, they don't involve such checks and therefore are somewhat faster. If you compile via the LLVM backend (-fllvm), that also takes advantage of the fact that you always divide by a single known number (10), and converts the division into multiplication and bit-shift. Time now: Total time 1.56s ( 1.56s elapsed).
Instead of using quot and rem separately, let's use the quotRem function that computes both at once, so that we don't repeat the division (even with multiplication+shift that takes a little time):
giveResult x = case x `quotRem` 10 of
(q,r) -> r*r + giveResult q
That doesn't gain much, but a little: Total time 1.49s ( 1.49s elapsed).
You're using a list a = [1 .. 10000000], and map the function over that list and then sum the resulting list. That's idiomatic, neat and short, but not super fast, since allocating all those list cells and garbage collecting them takes time too - not very much, since GHC is very good at that, but transforming it into a loop
main = print $ go 0 1
where
go acc n
| n > 10000000 = acc
| otherwise = go (acc + myFirstFunction n) (n+1)
gains us a little still: Total time 1.34s ( 1.34s elapsed) and the allocation dropped from 880,051,856 bytes allocated in the heap for the last list version to 51,840 bytes allocated in the heap.
giveResult is recursive, and therefore cannot be inlined. The same holds for myFirstFunction, hence each computation needs two function calls (at least). We can avoid that by rewriting giveResult to a non-recursive wrapper and a recursive local loop,
giveResult x = go 0 x
where
go acc 0 = acc
go acc n = case n `quotRem` 10 of
(q,r) -> go (acc + r*r) q
so that that can be inlined: Total time 1.04s ( 1.04s elapsed).
Those were the most obvious points, further improvements - apart from the memoisation mentioned by hammar in the comments - would need some thinking.
We are now at
module Main (main) where
myFirstFunction :: Int -> Int
myFirstFunction 1 = 0
myFirstFunction 89 = 1
myFirstFunction x= myFirstFunction (giveResult x)
giveResult :: Int -> Int
giveResult x = go 0 x
where
go acc 0 = acc
go acc n = case n `quotRem` 10 of
(q,r) -> go (acc + r*r) q
main :: IO ()
main = print $ go 0 1
where
go acc n
| n > 10000000 = acc
| otherwise = go (acc + myFirstFunction n) (n+1)
With -O2 -fllvm, that runs in 1.04 seconds here, but with the native code generator (only -O2), it takes 3.5 seconds. That difference is due to the fact that GHC itself doesn't convert the division into a multiplication and bit-shift. If we do it by hand, we get pretty much the same performance from the native code generator.
Because we know something that the compiler doesn't, namely that we never deal with negative numbers here, and the numbers don't become large, we can even generate a better multiply-and-shift (that would produce wrong results for negative or large dividends) than the compiler and take the time down to 0.9 seconds for the native code generator and 0.73 seconds for the LLVM backend:
import Data.Bits
qr10 :: Int -> (Int, Int)
qr10 n = (q, r)
where
q = (n * 0x66666667) `unsafeShiftR` 34
r = n - 10 * q
Note: That requires that Int is a 64-bit type, it won't work with 32-bit Ints, it will produce wrong results for negative n, and the multiplication will overflow for large n. We're entering dirty-hack territory. We can alleviate the dirtyness by using Word instead of Int, that leaves only the overflow (which doesn't occur for n <= 10737418236 with Word resp n <= 5368709118 for Int, so here we are comfortably in the safe zone). The times aren't affected.
The corresponding C programme
#include <stdio.h>
unsigned int myFirstFunction(unsigned int i);
unsigned int giveResult(unsigned int i);
int main(void) {
unsigned int sum = 0;
for(unsigned int i = 1; i <= 10000000; ++i) {
sum += myFirstFunction(i);
}
printf("%u\n",sum);
return 0;
}
unsigned int myFirstFunction(unsigned int i) {
if (i == 1) return 0;
if (i == 89) return 1;
return myFirstFunction(giveResult(i));
}
unsigned int giveResult(unsigned int i) {
unsigned int acc = 0, r, q;
while(i) {
q = (i*0x66666667UL) >> 34;
r = i - q*10;
i = q;
acc += r*r;
}
return acc;
}
performs similarly, compiled with gcc -O3, it runs in 0.78 seconds, and with clang -O3 in 0.71.
That's pretty much the end without changing the algorithm.
Memoisation
Now, a minor change of algorithm is memoisation. If we build a lookup table for the numbers <= 7*9², we need only one computation of the sum of the squares of the digits for each number rather than iterating that until we reach 1 or 89, so let's memoise,
module Main (main) where
import Data.Array.Unboxed
import Data.Array.IArray
import Data.Array.Base (unsafeAt)
import Data.Bits
qr10 :: Int -> (Int, Int)
qr10 n = (q, r)
where
q = (n * 0x66666667) `unsafeShiftR` 34
r = n - 10 * q
digitSquareSum :: Int -> Int
digitSquareSum = go 0
where
go acc 0 = acc
go acc n = case qr10 n of
(q,r) -> go (acc + r*r) q
table :: UArray Int Int
table = array (0,567) $ assocs helper
where
helper :: Array Int Int
helper = array (0,567) [(i, f i) | i <- [0 .. 567]]
f 0 = 0
f 1 = 0
f 89 = 1
f n = helper ! digitSquareSum n
endPoint :: Int -> Int
endPoint n = table `unsafeAt` digitSquareSum n
main :: IO ()
main = print $ go 0 1
where
go acc n
| n > 10000000 = acc
| otherwise = go (acc + endPoint n) (n+1)
Doing the memoisation by hand instead of using a library makes the code longer, but we can tailor it to our needs. We can use an unboxed array, and we can omit the bounds check on the array access. Both significantly speed the computation up. The time is now 0.18 seconds for the native code generator, and 0.13 seconds for the LLVM backend. The corresponding C programme runs in 0.16 seconds compiled with gcc -O3, and 0.145 seconds compiled with clang -O3 (Haskell beats C, w00t!).
Scaling and a hint for a better algorithm
The used algorithm however doesn't scale too well, a bit worse than linear, and for an upper bound of 108 (with suitably adapted memoisation limit), it runs in 1.5 seconds (ghc -O2 -fllvm), resp. 1.64 seconds (clang -O3) and 1.87 seconds (gcc -O3) [2.02 seconds for the native code generator].
Using a different algorithm that counts the numbers whose sequence ends in 1 by partitioning such numbers into a sum of squares of digits (The only numbers that directly produce 1 are powers of 10. We can write
10 = 1×3² + 1×1²
10 = 2×2² + 2×1²
10 = 1×2² + 6×1²
10 = 10×1²
From the first, we obtain 13, 31, 103, 130, 301, 310, 1003, 1030, 1300, 3001, 3010, 3100, ...
From the second, we obtain 1122, 1212, 1221, 2112, 2121, 2211, 11022, 11202, ...
From the third 1111112, 1111121, ...
Only 13, 31, 103, 130, 301, 310 are possible sums of squares of the digits of numbers <= 10^10, so only those need to be investigated further. We can write
100 = 1×9² + 1×4² + 3×1²
...
100 = 1×8² + 1×6²
...
The first of these partitions generates no children since it requires five nonzero digits, the other explicitly given generates the two children 68 and 86 (also 608 if the limit is 108, more for larger limits)), we can get better scaling and a faster algorithm.
The fairly unoptimised programme I wrote way back when to solve this problem runs (input is exponent of 10 of the limit)
$ time ./problem92 7
8581146
real 0m0.010s
user 0m0.008s
sys 0m0.002s
$ time ./problem92 8
85744333
real 0m0.022s
user 0m0.018s
sys 0m0.003s
$ time ./problem92 9
854325192
real 0m0.040s
user 0m0.033s
sys 0m0.006s
$ time ./problem92 10
8507390852
real 0m0.074s
user 0m0.069s
sys 0m0.004s
in a different league.
First off, I took the liberty of cleaning up your code a little:
endsAt89 1 = 0
endsAt89 89 = 1
endsAt89 n = endsAt89 (sumOfSquareDigits n)
sumOfSquareDigits 0 = 0
sumOfSquareDigits n = (n `mod` 10)^2 + sumOfSquareDigits (n `div` 10)
main = print . sum $ map endsAt89 [1..10^7]
On my crappy netbook is 1 min 13 sec. Let's see if we can improve that.
Since the numbers are small, we can start by using machine-sized Int instead of arbitrary-size Integer. This is just a matter of adding type signatures, e.g.
sumOfSquareDigits :: Int -> Int
That improves the run time drastically to 20 seconds.
Since the numbers are all positive, we can replace div and mod with the slightly faster quot and rem, or even both in one go with quotRem:
sumOfSquareDigits :: Int -> Int
sumOfSquareDigits 0 = 0
sumOfSquareDigits n = r^2 + sumOfSquareDigits q
where (q, r) = quotRem x 10
Run time is now 17 seconds. Making it tail recursive shaves off another second:
sumOfSquareDigits :: Int -> Int
sumOfSquareDigits n = loop n 0
where
loop 0 !s = s
loop n !s = loop q (s + r^2)
where (q, r) = quotRem n 10
For further improvements, we can notice that sumOfSquareDigits returns at most 567 = 7 * 9^2 for the given input numbers, so we can memoize for small numbers to reduce the number of iterations needed. Here's my final version (using the data-memocombinators package for the memoization):
{-# LANGUAGE BangPatterns #-}
import qualified Data.MemoCombinators as Memo
endsAt89 :: Int -> Int
endsAt89 = Memo.arrayRange (1, 7*9^2) endsAt89'
where
endsAt89' 1 = 0
endsAt89' 89 = 1
endsAt89' n = endsAt89 (sumOfSquareDigits n)
sumOfSquareDigits :: Int -> Int
sumOfSquareDigits n = loop n 0
where
loop 0 !s = s
loop n !s = loop q (s + r^2)
where (q, r) = quotRem n 10
main = print . sum $ map endsAt89 [1..10^7]
This runs in just under 9 seconds on my machine.