├── scripts ├── main.ts ├── api.ts ├── utils.ts ├── solve.ts └── scaffold.ts ├── .env.example ├── bun.lockb ├── src ├── 03 │ ├── example.txt │ ├── 03.ts │ └── 03.test.ts ├── 01 │ ├── example.txt │ ├── 01.test.ts │ └── 01.ts ├── 04 │ ├── example.txt │ ├── 04.test.ts │ └── 04.ts └── 02 │ ├── example.txt │ ├── 02.test.ts │ └── 02.ts ├── global.d.ts ├── devbox.json ├── .envrc ├── package.json ├── tsconfig.json ├── LICENSE ├── README.md ├── devbox.lock └── .gitignore /scripts/main.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SESSION= 2 | YEAR=2023 3 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hughevans/advent-2025/main/bun.lockb -------------------------------------------------------------------------------- /src/03/example.txt: -------------------------------------------------------------------------------- 1 | 987654321111111 2 | 811111111111119 3 | 234234234234278 4 | 818181911112111 5 | -------------------------------------------------------------------------------- /src/01/example.txt: -------------------------------------------------------------------------------- 1 | L68 2 | L30 3 | R48 4 | L5 5 | R60 6 | L55 7 | L1 8 | L99 9 | R14 10 | L82 11 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "bun" { 2 | interface Env { 3 | SESSION: string 4 | YEAR: string 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/04/example.txt: -------------------------------------------------------------------------------- 1 | ..@@.@@@@. 2 | @@@.@.@.@@ 3 | @@@@@.@.@@ 4 | @.@@@@..@. 5 | @@.@@@@.@@ 6 | .@@@@@@@.@ 7 | .@.@.@.@@@ 8 | @.@@@.@@@@ 9 | .@@@@@@@@. 10 | @.@.@@@.@. 11 | -------------------------------------------------------------------------------- /devbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/jetpack-io/devbox/main/.schema/devbox.schema.json", 3 | "packages": ["bun@latest"], 4 | "env": {}, 5 | "shell": {} 6 | } 7 | -------------------------------------------------------------------------------- /src/02/example.txt: -------------------------------------------------------------------------------- 1 | 11-22,95-115,998-1012,1188511880-1188511890,222220-222224, 2 | 1698522-1698528,446443-446449,38593856-38593862,565653-565659, 3 | 824824821-824824827,2121212118-2121212124 4 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # Automatically sets up your devbox environment whenever you cd into this 2 | # directory via our direnv integration: 3 | 4 | eval "$(devbox generate direnv --print-envrc)" 5 | 6 | # check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/ 7 | # for more details 8 | -------------------------------------------------------------------------------- /scripts/api.ts: -------------------------------------------------------------------------------- 1 | import { isOk } from './utils.ts' 2 | 3 | const headers = { 4 | Cookie: `session=${process.env.SESSION}` 5 | } 6 | 7 | export function fetchInput({ day, year }: { day: number; year: number }) { 8 | return fetch(`https://adventofcode.com/${year}/day/${day}/input`, { 9 | headers 10 | }) 11 | .then(isOk) 12 | .then(response => response.text()) 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advent-of-code-bun", 3 | "module": "scripts/main.ts", 4 | "type": "module", 5 | "scripts": { 6 | "solve": "bun --watch ./scripts/solve.ts" 7 | }, 8 | "devDependencies": { 9 | "bun-types": "latest", 10 | "chalk": "^5.6.2", 11 | "dedent": "^1.7.0", 12 | "prettier": "^3.7.3" 13 | }, 14 | "peerDependencies": { 15 | "typescript": "^5.9.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/03/03.ts: -------------------------------------------------------------------------------- 1 | export function parse(input: string) { 2 | return input.trim().split("\n"); 3 | } 4 | 5 | export function partOne(input: ReturnType) { 6 | let totalJoltage = 0; 7 | 8 | for (const bank of input) { 9 | let maxJoltage = 0; 10 | 11 | for (let i = 0; i < bank.length; i++) { 12 | for (let j = i + 1; j < bank.length; j++) { 13 | const joltage = parseInt(bank[i] + bank[j]); 14 | maxJoltage = Math.max(maxJoltage, joltage); 15 | } 16 | } 17 | 18 | totalJoltage += maxJoltage; 19 | } 20 | 21 | return totalJoltage; 22 | } 23 | 24 | export function partTwo(input: ReturnType) {} 25 | -------------------------------------------------------------------------------- /src/03/03.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "bun:test"; 2 | import { parse, partOne, partTwo } from "./03"; 3 | import { readFileSync } from "fs"; 4 | 5 | const example = readFileSync(`${import.meta.dir}/example.txt`, "utf-8"); 6 | const input = readFileSync(`${import.meta.dir}/input.txt`, "utf-8"); 7 | 8 | describe("Day 3", () => { 9 | describe("Part One", () => { 10 | test("example", () => { 11 | expect(partOne(parse(example))).toBe(357); 12 | }); 13 | 14 | test("input", () => { 15 | const result = partOne(parse(input)); 16 | console.log("Part One:", result); 17 | }); 18 | }); 19 | 20 | describe("Part Two", () => {}); 21 | }); 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": false, 13 | "downlevelIteration": true, 14 | "skipLibCheck": true, 15 | "jsx": "preserve", 16 | "allowSyntheticDefaultImports": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "allowJs": true, 19 | "checkJs": true, 20 | "types": [ 21 | "bun-types" 22 | ], 23 | "baseUrl": "./", 24 | "paths": { 25 | "@/*": ["./src/*"] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/utils.ts: -------------------------------------------------------------------------------- 1 | export function withPerformance(handler: () => T) { 2 | const start = performance.now(); 3 | const result = handler(); 4 | const end = performance.now(); 5 | 6 | return [result, end - start] as const; 7 | } 8 | 9 | export function formatPerformance(time: number) { 10 | const round = (x: number) => Math.round((x + Number.EPSILON) * 100) / 100; 11 | if (time < 1) return `${round(time * 1000)} µs`; 12 | return `${round(time)} ms`; 13 | } 14 | 15 | export function isBetween(x: number, [min, max]: [number, number]) { 16 | return x >= min && x <= max; 17 | } 18 | 19 | export function isOk(response: Response): Promise { 20 | return new Promise((resolve, reject) => 21 | response.ok ? resolve(response) : reject(response) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/01/01.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "bun:test"; 2 | import { parse, partOne, partTwo } from "./01"; 3 | import { readFileSync } from "fs"; 4 | 5 | const example = readFileSync(`${import.meta.dir}/example.txt`, "utf-8"); 6 | const input = readFileSync(`${import.meta.dir}/input.txt`, "utf-8"); 7 | 8 | describe("Day 1", () => { 9 | describe("Part One", () => { 10 | test("example", () => { 11 | expect(partOne(parse(example))).toBe(3); 12 | }); 13 | 14 | test("input", () => { 15 | const result = partOne(parse(input)); 16 | console.log("Part One:", result); 17 | }); 18 | }); 19 | 20 | describe("Part Two", () => { 21 | test("example", () => { 22 | expect(partTwo(parse(example))).toBe(6); 23 | }); 24 | 25 | test("input", () => { 26 | const result = partTwo(parse(input)); 27 | console.log("Part Two:", result); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/04/04.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "bun:test"; 2 | import { parse, partOne, partTwo } from "./04"; 3 | import { readFileSync } from "fs"; 4 | 5 | const example = readFileSync(`${import.meta.dir}/example.txt`, "utf-8"); 6 | const input = readFileSync(`${import.meta.dir}/input.txt`, "utf-8"); 7 | 8 | describe("Day 4", () => { 9 | describe("Part One", () => { 10 | test("example", () => { 11 | expect(partOne(parse(example))).toBe(13); 12 | }); 13 | 14 | test("input", () => { 15 | const result = partOne(parse(input)); 16 | console.log("Part One:", result); 17 | }); 18 | }); 19 | 20 | describe("Part Two", () => { 21 | test("example", () => { 22 | expect(partTwo(parse(example))).toBe(43); 23 | }); 24 | 25 | test("input", () => { 26 | const result = partTwo(parse(input)); 27 | console.log("Part Two:", result); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/02/02.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "bun:test"; 2 | import { parse, partOne, partTwo } from "./02"; 3 | import { readFileSync } from "fs"; 4 | 5 | const example = readFileSync(`${import.meta.dir}/example.txt`, "utf-8"); 6 | const input = readFileSync(`${import.meta.dir}/input.txt`, "utf-8"); 7 | 8 | describe("Day 2", () => { 9 | describe("Part One", () => { 10 | test("example", () => { 11 | expect(partOne(parse(example))).toBe(1227775554); 12 | }); 13 | 14 | test("input", () => { 15 | const result = partOne(parse(input)); 16 | console.log("Part One:", result); 17 | }); 18 | }); 19 | 20 | describe("Part Two", () => { 21 | test("example", () => { 22 | expect(partTwo(parse(example))).toBe(4174379265); 23 | }); 24 | 25 | test("input", () => { 26 | const result = partTwo(parse(input)); 27 | console.log("Part Two:", result); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Adrian Klimek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/solve.ts: -------------------------------------------------------------------------------- 1 | import { argv } from 'bun' 2 | import chalk from 'chalk' 3 | import { formatPerformance, withPerformance, isBetween } from './utils.ts' 4 | import { scaffold } from './scaffold.ts' 5 | 6 | const day = parseInt(argv[2] ?? '') 7 | const year = parseInt(process.env.YEAR ?? new Date().getFullYear()) 8 | 9 | if (!isBetween(day, [1, 25])) { 10 | console.log(`🎅 Pick a day between ${chalk.bold(1)} and ${chalk.bold(25)}.`) 11 | console.log(`🎅 To get started, try: ${chalk.cyan('bun solve 1')}`) 12 | process.exit(0) 13 | } 14 | 15 | await scaffold(day, year) 16 | 17 | const name = `${day}`.padStart(2, '0') 18 | 19 | const { default: input } = await import(`@/${name}/input.txt`) 20 | const { partOne, partTwo, parse } = await import(`@/${name}/${name}.ts`) 21 | 22 | const [one, onePerformance] = withPerformance(() => partOne?.(parse(input))) 23 | const [two, twoPerformance] = withPerformance(() => partTwo?.(parse(input))) 24 | 25 | console.log( 26 | '🌲', 27 | 'Part One:', 28 | chalk.green(one ?? '—'), 29 | one ? `(${formatPerformance(onePerformance)})` : '' 30 | ) 31 | console.log( 32 | '🎄', 33 | 'Part Two:', 34 | chalk.green(two ?? '—'), 35 | two ? `(${formatPerformance(twoPerformance)})` : '' 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code with Bun 2 | 3 | A template repository for solving Advent of Code and experimenting with Bun runtime. 4 | 5 | ## Getting started 6 | 7 | 1. Generate your repository using [this template](https://github.com/adrianklimek/advent-of-code-bun/generate). 8 | 2. Make sure you have installed [Bun](https://bun.sh/docs/installation#installing). 9 | 3. Install dependencies: 10 | 11 | ```bash 12 | bun install 13 | ``` 14 | 15 | 4. Create `.env` file based on `.env.example`. 16 | 5. (Optional) Set your session token with environment variables to automatically fetch your input. You can obtain the session token from the AoC session cookie. 17 | 18 | ## Running the Code 19 | 20 | To run any solution you have to run the `solve` script. It will create all directories and files for a day, and also it can fetch your input file. Besides that, it watches all the changes you make and shows a result in a terminal. 21 | 22 | ### Example usage 23 | 24 | To run a solution for the first day: 25 | 26 | ```bash 27 | bun solve 1 28 | ``` 29 | 30 | To run tests in watch mode: 31 | 32 | ```bash 33 | bun test --watch 34 | ``` 35 | 36 | ## Structure 37 | 38 | For each day a directory in `src` is created with the following structure: 39 | 40 | ```bash 41 | 📂 01 42 | ├── 📜 01.ts 43 | ├── 📜 01.test.ts 44 | ├── 📜 example.txt 45 | └── 📜 input.txt 46 | ``` 47 | 48 | ## Closing words 49 | 50 | Happy coding! 🎄✨ 51 | -------------------------------------------------------------------------------- /scripts/scaffold.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import dedent from "dedent"; 3 | import { existsSync } from "node:fs"; 4 | import { mkdir } from "node:fs/promises"; 5 | 6 | import { fetchInput } from "./api.ts"; 7 | 8 | export async function scaffold(day: number, year: number) { 9 | const name = `${day}`.padStart(2, "0"); 10 | 11 | const directory = new URL(`../src/${name}/`, import.meta.url); 12 | 13 | if (existsSync(directory)) return; 14 | 15 | console.log(`📂 Setting up day ${day} of ${year}`); 16 | 17 | await mkdir(directory); 18 | 19 | const test = dedent` 20 | import { describe } from 'bun:test' 21 | 22 | describe(${`'Day ${day}'`}, () => { 23 | describe('Part One', () => {}) 24 | 25 | describe('Part Two', () => {}) 26 | }) 27 | `; 28 | 29 | const solution = dedent` 30 | export function parse(input: string) { 31 | return input 32 | } 33 | 34 | export function partOne(input: ReturnType) {} 35 | 36 | export function partTwo(input: ReturnType) {} 37 | `; 38 | 39 | console.log(`📂 Fetching your input`); 40 | 41 | const input = await fetchInput({ day, year }).catch(() => { 42 | console.log( 43 | chalk.red.bold( 44 | "📂 Fetching your input have failed, empty file will be created." 45 | ) 46 | ); 47 | }); 48 | 49 | await Bun.write(new URL(`${name}.test.ts`, directory.href), test); 50 | await Bun.write(new URL(`${name}.ts`, directory.href), solution); 51 | await Bun.write(new URL(`input.txt`, directory.href), input ?? ""); 52 | await Bun.write(new URL(`example.txt`, directory.href), ""); 53 | 54 | console.log("📂 You all set up, have fun!"); 55 | } 56 | -------------------------------------------------------------------------------- /src/01/01.ts: -------------------------------------------------------------------------------- 1 | export function parse(input: string) { 2 | return input 3 | .trim() 4 | .split("\n") 5 | .map((line) => { 6 | const direction = line[0] as "L" | "R"; 7 | const distance = parseInt(line.slice(1)); 8 | return { direction, distance }; 9 | }); 10 | } 11 | 12 | export function partOne(input: ReturnType) { 13 | let position = 50; 14 | let zeroCount = 0; 15 | 16 | for (const { direction, distance } of input) { 17 | if (direction === "L") { 18 | position = (position - distance + 100 * Math.ceil(distance / 100)) % 100; 19 | } else { 20 | position = (position + distance) % 100; 21 | } 22 | 23 | if (position === 0) { 24 | zeroCount++; 25 | } 26 | } 27 | 28 | return zeroCount; 29 | } 30 | 31 | export function partTwo(input: ReturnType) { 32 | let position = 50; 33 | let zeroCount = 0; 34 | 35 | for (const { direction, distance } of input) { 36 | if (direction === "L") { 37 | if (position === 0) { 38 | zeroCount += Math.floor(distance / 100); 39 | } else { 40 | if (distance >= position) { 41 | zeroCount += 1 + Math.floor((distance - position) / 100); 42 | } 43 | } 44 | 45 | position = (((position - distance) % 100) + 100) % 100; 46 | } else { 47 | if (position === 0) { 48 | zeroCount += Math.floor(distance / 100); 49 | } else { 50 | const firstZero = 100 - position; 51 | if (distance >= firstZero) { 52 | zeroCount += 1 + Math.floor((distance - firstZero) / 100); 53 | } 54 | } 55 | 56 | position = (position + distance) % 100; 57 | } 58 | } 59 | 60 | return zeroCount; 61 | } 62 | -------------------------------------------------------------------------------- /src/04/04.ts: -------------------------------------------------------------------------------- 1 | export function parse(input: string) { 2 | return input 3 | .trim() 4 | .split("\n") 5 | .map((line) => line.split("")); 6 | } 7 | 8 | type Grid = ReturnType; 9 | 10 | const DIRECTIONS = [ 11 | [-1, -1], 12 | [-1, 0], 13 | [-1, 1], 14 | [0, -1], 15 | [0, 1], 16 | [1, -1], 17 | [1, 0], 18 | [1, 1], 19 | ]; 20 | 21 | function countAdjacentRolls(grid: Grid, row: number, col: number): number { 22 | let count = 0; 23 | for (const [dr, dc] of DIRECTIONS) { 24 | const nr = row + dr; 25 | const nc = col + dc; 26 | if (nr >= 0 && nr < grid.length && nc >= 0 && nc < grid[0].length) { 27 | if (grid[nr][nc] === "@") { 28 | count++; 29 | } 30 | } 31 | } 32 | return count; 33 | } 34 | 35 | export function partOne(input: Grid) { 36 | let accessibleCount = 0; 37 | 38 | for (let row = 0; row < input.length; row++) { 39 | for (let col = 0; col < input[0].length; col++) { 40 | if (input[row][col] === "@") { 41 | const adjacentRolls = countAdjacentRolls(input, row, col); 42 | if (adjacentRolls < 4) { 43 | accessibleCount++; 44 | } 45 | } 46 | } 47 | } 48 | 49 | return accessibleCount; 50 | } 51 | 52 | export function partTwo(input: Grid) { 53 | const grid = input.map((row) => [...row]); 54 | let totalRemoved = 0; 55 | 56 | while (true) { 57 | const toRemove: [number, number][] = []; 58 | 59 | for (let row = 0; row < grid.length; row++) { 60 | for (let col = 0; col < grid[0].length; col++) { 61 | if (grid[row][col] === "@") { 62 | const adjacentRolls = countAdjacentRolls(grid, row, col); 63 | if (adjacentRolls < 4) { 64 | toRemove.push([row, col]); 65 | } 66 | } 67 | } 68 | } 69 | 70 | if (toRemove.length === 0) { 71 | break; 72 | } 73 | 74 | for (const [row, col] of toRemove) { 75 | grid[row][col] = "."; 76 | } 77 | 78 | totalRemoved += toRemove.length; 79 | } 80 | 81 | return totalRemoved; 82 | } 83 | -------------------------------------------------------------------------------- /devbox.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfile_version": "1", 3 | "packages": { 4 | "bun@latest": { 5 | "last_modified": "2024-11-18T00:41:09Z", 6 | "resolved": "github:NixOS/nixpkgs/5083ec887760adfe12af64830a66807423a859a7#bun", 7 | "source": "devbox-search", 8 | "version": "1.1.34", 9 | "systems": { 10 | "aarch64-darwin": { 11 | "outputs": [ 12 | { 13 | "name": "out", 14 | "path": "/nix/store/xnj00gq1mfffvgyisghk4m9f38gc5c5c-bun-1.1.34", 15 | "default": true 16 | } 17 | ], 18 | "store_path": "/nix/store/xnj00gq1mfffvgyisghk4m9f38gc5c5c-bun-1.1.34" 19 | }, 20 | "aarch64-linux": { 21 | "outputs": [ 22 | { 23 | "name": "out", 24 | "path": "/nix/store/kccpzv8fb7swmmxbhv805qzq242rhy33-bun-1.1.34", 25 | "default": true 26 | } 27 | ], 28 | "store_path": "/nix/store/kccpzv8fb7swmmxbhv805qzq242rhy33-bun-1.1.34" 29 | }, 30 | "x86_64-darwin": { 31 | "outputs": [ 32 | { 33 | "name": "out", 34 | "path": "/nix/store/8h5k3l4fpgs3al2wvna7x9ybyyc8k36d-bun-1.1.34", 35 | "default": true 36 | } 37 | ], 38 | "store_path": "/nix/store/8h5k3l4fpgs3al2wvna7x9ybyyc8k36d-bun-1.1.34" 39 | }, 40 | "x86_64-linux": { 41 | "outputs": [ 42 | { 43 | "name": "out", 44 | "path": "/nix/store/x089g4srqw71w5jnc87vxbh7m12498i0-bun-1.1.34", 45 | "default": true 46 | } 47 | ], 48 | "store_path": "/nix/store/x089g4srqw71w5jnc87vxbh7m12498i0-bun-1.1.34" 49 | } 50 | } 51 | }, 52 | "github:NixOS/nixpkgs/nixpkgs-unstable": { 53 | "last_modified": "2025-11-22T10:07:53Z", 54 | "resolved": "github:NixOS/nixpkgs/878e468e02bfabeda08c79250f7ad583037f2227?lastModified=1763806073&narHash=sha256-FHsEKDvfWpzdADWj99z7vBk4D716Ujdyveo5%2BA048aI%3D" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | .idea 172 | 173 | */**/input.txt 174 | -------------------------------------------------------------------------------- /src/02/02.ts: -------------------------------------------------------------------------------- 1 | export function parse(input: string) { 2 | return input 3 | .trim() 4 | .replace(/\n/g, "") 5 | .split(",") 6 | .filter((s) => s.length > 0) 7 | .map((range) => { 8 | const [start, end] = range.split("-").map(Number); 9 | return { start, end }; 10 | }); 11 | } 12 | 13 | // Check if a number is made of a sequence repeated twice 14 | function isRepeatedSequence(n: number): boolean { 15 | const s = n.toString(); 16 | // Must have even length to be two repetitions 17 | if (s.length % 2 !== 0) return false; 18 | const half = s.length / 2; 19 | const first = s.slice(0, half); 20 | const second = s.slice(half); 21 | // First half can't start with 0 (no leading zeros) 22 | return first === second && first[0] !== "0"; 23 | } 24 | 25 | // Generate all repeated-sequence numbers within a range 26 | function findInvalidIdsInRange(start: number, end: number): number[] { 27 | const invalidIds: number[] = []; 28 | 29 | // For efficiency, generate candidate repeated numbers rather than checking every number 30 | // A repeated number has form XX where X is some digit sequence 31 | // The smallest is 11 (length 2), then 1010, 1111, etc. 32 | 33 | // Determine the range of lengths to check 34 | const minLen = start.toString().length; 35 | const maxLen = end.toString().length; 36 | 37 | for (let totalLen = 2; totalLen <= maxLen; totalLen += 2) { 38 | const halfLen = totalLen / 2; 39 | // Generate all possible half-sequences of this length 40 | const minHalf = halfLen === 1 ? 1 : Math.pow(10, halfLen - 1); 41 | const maxHalf = Math.pow(10, halfLen) - 1; 42 | 43 | for (let half = minHalf; half <= maxHalf; half++) { 44 | const repeated = Number(half.toString() + half.toString()); 45 | if (repeated >= start && repeated <= end) { 46 | invalidIds.push(repeated); 47 | } 48 | // Early exit if we've passed the end of range 49 | if (repeated > end) break; 50 | } 51 | } 52 | 53 | return invalidIds; 54 | } 55 | 56 | export function partOne(input: ReturnType) { 57 | let sum = 0; 58 | for (const { start, end } of input) { 59 | const invalidIds = findInvalidIdsInRange(start, end); 60 | for (const id of invalidIds) { 61 | sum += id; 62 | } 63 | } 64 | return sum; 65 | } 66 | 67 | export function partTwo(input: ReturnType) { 68 | let sum = 0; 69 | for (const { start, end } of input) { 70 | const invalidIds = findInvalidIdsInRangeV2(start, end); 71 | for (const id of invalidIds) { 72 | sum += id; 73 | } 74 | } 75 | return sum; 76 | } 77 | 78 | // Generate all repeated-sequence numbers (repeated at least twice) within a range 79 | function findInvalidIdsInRangeV2(start: number, end: number): number[] { 80 | const invalidIds = new Set(); 81 | const maxLen = end.toString().length; 82 | 83 | // For each possible total length 84 | for (let totalLen = 2; totalLen <= maxLen; totalLen++) { 85 | // For each possible pattern length that divides totalLen 86 | for (let patternLen = 1; patternLen <= totalLen / 2; patternLen++) { 87 | if (totalLen % patternLen !== 0) continue; 88 | 89 | const repetitions = totalLen / patternLen; 90 | if (repetitions < 2) continue; 91 | 92 | // Generate all possible patterns of this length 93 | const minPattern = patternLen === 1 ? 1 : Math.pow(10, patternLen - 1); 94 | const maxPattern = Math.pow(10, patternLen) - 1; 95 | 96 | for (let pattern = minPattern; pattern <= maxPattern; pattern++) { 97 | const patternStr = pattern.toString(); 98 | const repeated = Number(patternStr.repeat(repetitions)); 99 | 100 | if (repeated >= start && repeated <= end) { 101 | invalidIds.add(repeated); 102 | } 103 | 104 | // Early exit if the smallest possible number of this pattern length exceeds end 105 | if (repeated > end && pattern === minPattern) break; 106 | } 107 | } 108 | } 109 | 110 | return Array.from(invalidIds); 111 | } 112 | --------------------------------------------------------------------------------