type Point = { x: number; y: number }
type Edge = {
x1: number
y1: number
x2: number
y2: number
vertical: boolean
minX: number
maxX: number
minY: number
maxY: number
}
type Rectangle = {
area: number
first: Point
second: Point
}
const EPSILON = 1e-9
function parsePoints(input: string): Point[] {
if (!input.trim()) {
return []
}
return input
.trim()
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.map((line, idx) => {
const [xStr, yStr] = line.split(',').map((value) => value.trim())
if (xStr === undefined || yStr === undefined) {
throw new Error(`Invalid coordinate on line ${idx + 1}: "${line}"`)
}
const x = Number(xStr)
const y = Number(yStr)
if (Number.isNaN(x) || Number.isNaN(y)) {
throw new Error(`Invalid numeric value on line ${idx + 1}: "${line}"`)
}
return { x, y }
})
}
function inclusiveArea(a: Point, b: Point): number {
const width = Math.abs(a.x - b.x) + 1
const height = Math.abs(a.y - b.y) + 1
return width * height
}
function buildEdges(points: Point[]): Edge[] {
if (points.length < 2) {
return []
}
const edges: Edge[] = []
for (let i = 0; i < points.length; i++) {
const a = points[i]
const b = points[(i + 1) % points.length]
if (a.x !== b.x && a.y !== b.y) {
throw new Error(
`Invalid polygon: consecutive points are not aligned (${a.x},${a.y}) -> (${b.x},${b.y})`,
)
}
const vertical = a.x === b.x
const edge: Edge = {
x1: a.x,
y1: a.y,
x2: b.x,
y2: b.y,
vertical,
minX: Math.min(a.x, b.x),
maxX: Math.max(a.x, b.x),
minY: Math.min(a.y, b.y),
maxY: Math.max(a.y, b.y),
}
edges.push(edge)
}
return edges
}
function isPointOnSegment(
px: number,
py: number,
ax: number,
ay: number,
bx: number,
by: number,
): boolean {
if (Math.abs(ax - bx) < EPSILON) {
if (Math.abs(px - ax) > EPSILON) {
return false
}
return py + EPSILON >= Math.min(ay, by) && py - EPSILON <= Math.max(ay, by)
}
if (Math.abs(ay - by) < EPSILON) {
if (Math.abs(py - ay) > EPSILON) {
return false
}
return px + EPSILON >= Math.min(ax, bx) && px - EPSILON <= Math.max(ax, bx)
}
return false
}
function pointInPolygon(point: Point, polygon: Point[]): boolean {
const { x: px, y: py } = point
let inside = false
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x
const yi = polygon[i].y
const xj = polygon[j].x
const yj = polygon[j].y
if (isPointOnSegment(px, py, xi, yi, xj, yj)) {
return true
}
if (Math.abs(yj - yi) < EPSILON) {
continue
}
const intersects =
(yi > py) !== (yj > py) &&
px <
((xj - xi) * (py - yi)) / (yj - yi + Number.EPSILON) + xi + EPSILON
if (intersects) {
inside = !inside
}
}
return inside
}
function rectangleIntersectsBoundary(
xMin: number,
xMax: number,
yMin: number,
yMax: number,
edges: Edge[],
): boolean {
for (const edge of edges) {
if (edge.vertical) {
if (edge.x1 <= xMin || edge.x1 >= xMax) {
continue
}
const overlap =
Math.min(edge.maxY, yMax) - Math.max(edge.minY, yMin)
if (overlap > 0) {
return true
}
} else {
if (edge.y1 <= yMin || edge.y1 >= yMax) {
continue
}
const overlap =
Math.min(edge.maxX, xMax) - Math.max(edge.minX, xMin)
if (overlap > 0) {
return true
}
}
}
return false
}
function isRectangleValid(
a: Point,
b: Point,
polygon: Point[],
edges: Edge[],
): boolean {
const xMin = Math.min(a.x, b.x)
const xMax = Math.max(a.x, b.x)
const yMin = Math.min(a.y, b.y)
const yMax = Math.max(a.y, b.y)
if (xMin === xMax && yMin === yMax) {
return false
}
if (rectangleIntersectsBoundary(xMin, xMax, yMin, yMax, edges)) {
return false
}
const sampleX = xMin === xMax ? xMin : (xMin + xMax) / 2
const sampleY = yMin === yMax ? yMin : (yMin + yMax) / 2
return pointInPolygon({ x: sampleX, y: sampleY }, polygon)
}
function formatRectangle(rect: Rectangle | null) {
if (!rect) {
return null
}
return {
area: rect.area,
first: rect.first,
second: rect.second,
}
}
export async function solve(input: string): Promise<string | number | object> {
const points = parsePoints(input)
if (points.length < 2) {
return {
part1: 0,
part2: 0,
details: {
totalPoints: points.length,
part1Best: null,
part2Best: null,
},
}
}
const edges = buildEdges(points)
let bestAny: Rectangle | null = null
let bestRestricted: Rectangle | null = null
let comparisons = 0
let validations = 0
for (let i = 0; i < points.length; i++) {
const pointA = points[i]
for (let j = i + 1; j < points.length; j++) {
const pointB = points[j]
const area = inclusiveArea(pointA, pointB)
comparisons += 1
if (!bestAny || area > bestAny.area) {
bestAny = { area, first: pointA, second: pointB }
}
if (bestRestricted && area <= bestRestricted.area) {
continue
}
validations += 1
if (isRectangleValid(pointA, pointB, points, edges)) {
bestRestricted = { area, first: pointA, second: pointB }
}
}
}
return {
part1: bestAny?.area ?? 0,
part2: bestRestricted?.area ?? 0,
details: {
totalPoints: points.length,
comparisons,
validationsAttempted: validations,
part1Best: formatRectangle(bestAny),
part2Best: formatRectangle(bestRestricted),
},
}
}
Create a file at src/solvers/day09.ts with the following structure:
export async function solve(input: string): Promise<string | number | object> {
// Your solution here
// The input parameter contains the puzzle input as a string
// Example:
const lines = input.trim().split('\n');
// Process and return your answer
return 'Your answer here';
}Then, import it in src/solvers/index.ts and add it to the solvers object:
import * as day09 from './day09'
export const solvers = {
// ... existing solvers
'09': day09,
}The solver function will receive the puzzle input as a string and should return the solution (string, number, or object).