adventofcode-2021/5/ventnavigator.hs

136 lines
4.2 KiB
Haskell

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
]