import Data.List (nub, tails) import Parsing (splitByString) type Line = ((Int, Int), (Int, Int)) main :: IO () main = do input <- getContents let vents = parseVents input putStrLn (show (solution1 vents)) solution1 :: [Line] -> Int solution1 = length . nub . intersectAll . filterDiagonal intersectAll :: [Line] -> [(Int, Int)] intersectAll = flatten . (map intersectOne) . (combinations 2) where intersectOne :: [Line] -> [(Int, Int)] intersectOne [] = [] intersectOne (vent:others) = flatten (map (intersect vent) others) intersect :: Line -> Line -> [(Int, Int)] intersect ((x1, y1), (x1', y1')) ((x2, y2), (x2', y2')) -- Colinear - In these cases, we may have multiple intersections | all (== x1) [x1', x2, x2'] = (map (\y -> (x1,y)) (intersect1D (sortedTuple (y1, y1')) (sortedTuple (y2, y2')))) | all (== y1) [y1', y2, y2'] = (map (\x -> (x,y1)) (intersect1D (sortedTuple (x1, x1')) (sortedTuple (x2, x2')))) -- Not colinear - We can find out if these intersect by simply -- checking whether the second line goes through the -- horizontal/vertical range of the first (whichever line the first -- segment lives on) | (x1 == x1' && y2 == y2') = if (min x2 x2') <= x1 && (max x2 x2') >= x1 && (min y1 y1') <= y2 && (max y1 y1') >= y2 then [(x1,y2)] else [] | (y1 == y1' && x2 == x2') = if (min y2 y2') <= y1 && (max y2 y2') >= y1 && (min x1 x1') <= x2 && (max x1 x1') >= x2 then [(x2,y1)] else [] -- In all other cases, we consider the lines not to intersect, since -- diagonal lines are out of scope | otherwise = [] where sortedTuple :: (Int, Int) -> (Int, Int) sortedTuple (a, b) | a > b = (b, a) | otherwise = (a, b) intersect1D :: (Int, Int) -> (Int, Int) -> [Int] intersect1D (a, b) (c, d) = let start = max a c end = min b d in [start..end] filterDiagonal :: [Line] -> [Line] filterDiagonal = filter (not . isDiagonal) where isDiagonal :: Line -> Bool isDiagonal ((x, y), (x', y')) = x /= x' && y /= y' parseVents :: String -> [Line] parseVents = tupelize . splitUp where tupelize :: [[[Int]]] -> [Line] tupelize = map subTupelize where subTupelize :: [[Int]] -> Line subTupelize [[a, b], [c, d]] = ((a, b), (c, d)) -- For our parsing purposes, we don't need to cover error -- cases subTupelize _ = undefined splitUp :: String -> [[[Int]]] splitUp = (map (map (map read))) . (map (map (splitByString ","))) . (map (splitByString " -> ")) . lines -- Missing stdlib functions combinations :: Int -> [a] -> [[a]] combinations 0 _ = [[]] combinations n xs = [y:ys | y:xs' <- tails xs , ys <- combinations (n-1) xs'] flatten :: [[a]] -> [a] flatten = foldr1 (++) -- Tests testInput1 :: String testInput1 = unlines [ "0,9 -> 5,9", "8,0 -> 0,8", "9,4 -> 3,4", "2,2 -> 2,1", "7,0 -> 7,4", "6,4 -> 2,0", "0,9 -> 2,9", "3,4 -> 1,4", "0,0 -> 8,8", "5,5 -> 8,2" ] test1 = solution1 (parseVents testInput1) testIntersect1 = intersect ((0, 0), (0, 5)) ((0, 2), (0, 4)) == [(0, 2), (0, 3), (0, 4)] testIntersect2 = intersect ((0, 0), (0, 5)) ((0, 5), (0, 6)) == [(0, 5)] testIntersect3 = intersect ((0, 0), (0, 5)) ((0, 6), (0, 7)) == [] testIntersect4 = intersect ((0, 0), (5, 0)) ((2, 0), (4, 0)) == [(2, 0), (3, 0), (4, 0)] testIntersect5 = intersect ((0, 0), (0, 5)) ((0, 2), (4, 2)) == [(0, 2)] testIntersect6 = intersect ((0, 0), (0, 5)) ((0, 3), (4, 3)) == [(0, 3)] testIntersect7 = intersect ((1, 0), (1, 5)) ((0, 2), (4, 2)) == [(1, 2)] testIntersect8 = intersect ((4, 0), (4, 5)) ((0, 2), (4, 2)) == [(4, 2)] testIntersect9 = intersect ((1, 0), (1, 5)) ((2, 2), (4, 2)) == [] testIntersect10 = intersect ((9, 4), (3, 4)) ((7, 0), (7, 4)) == [(7, 4)] testIntersect11 = intersect ((2, 2), (2, 1)) ((3, 4), (1, 4)) == [] testIntersect12 = intersect ((0, 9), (5, 9)) ((7, 0), (7, 4)) == [] testIntersect = all (== True) [ testIntersect1, testIntersect2, testIntersect3, testIntersect4, testIntersect5, testIntersect6, testIntersect7, testIntersect8, testIntersect9 ]