From a74802b55e25582dc5d5762d742f8c3689f92344 Mon Sep 17 00:00:00 2001 From: kitsunecafe Date: Thu, 12 Dec 2024 23:18:43 -0600 Subject: [PATCH] initial commit --- nine/package.json | 14 ++ nine/src/automaton.js | 341 ++++++++++++++++++++++++++++++++++++++++++ nine/src/index.js | 143 ++++++++++++++++++ 3 files changed, 498 insertions(+) create mode 100644 nine/package.json create mode 100644 nine/src/automaton.js create mode 100644 nine/src/index.js diff --git a/nine/package.json b/nine/package.json new file mode 100644 index 0000000..43a8ce1 --- /dev/null +++ b/nine/package.json @@ -0,0 +1,14 @@ +{ + "name": "nine", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node src/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "" +} diff --git a/nine/src/automaton.js b/nine/src/automaton.js new file mode 100644 index 0000000..89c64ef --- /dev/null +++ b/nine/src/automaton.js @@ -0,0 +1,341 @@ +const seek = value => () => value + +export const Seek = Object.freeze({ + Left: seek(-1), + Right: seek(1), + Stay: seek(0), + First: reader => -reader.position, + Last: reader => (reader.length - 1) - reader.position, + JumpFromStack: reader => { + const state = reader.pop() + reader.push() + return state.position - reader.position + }, + SeekFromStack: consuming => reader => { + const position = (consuming ? reader.peek() : reader.pop()).position + return position - reader.position + } +}) + +export const Predicate = Object.freeze({ + AnyOf: (...symbols) => b => symbols.includes(b.symbol), + Every: (...fns) => x => fns.every(fn => fn(x)), + Equals: a => b => a === b.symbol, + False: () => false, + InBounds: direction => reader => reader.inBounds(reader.position + direction()), + Not: fn => x => !fn(x), + Number: x => !isNaN(x.symbol), + OutOfBounds: direction => reader => !Predicate.InBounds(direction)(reader), + Some: (...fns) => x => fns.some(fn => fn(x)), + True: () => true, +}) + +const tee = fn => x => { + fn(x) + return x.symbol +} + +export const Replace = Object.freeze({ + Seq: (...fns) => x => fns.reduce((_acc, fn) => fn(x), x.symbol), + With: x => () => x, + Nothing: x => x.symbol, + Push: x => { + x.push() + return x.symbol + }, + Pop: x => x.pop(), + Swap: x => { + const tmp = x.pop() + x.push() + x.push(tmp) + return tmp.symbol + }, + ClearStack: tee(x => x.clear()), + FromStack: x => x.peek().symbol, + Tee: tee +}) + + +class Head { + #position = 0 + + get position() { return this.#position } + + left() { this.move(Left) } + right() { this.move(Right) } + move(value) { this.#position += value } + + + read(tape) { + return tape.at(this.#position) + } + + write(tape, value) { + tape.write(this.#position, value) + } +} + +export class Tape { + #value + + constructor(input) { + this.#value = Array.isArray(input) ? input : input.split('') + } + + get value() { return this.#value } + get length() { return this.#value.length } + + at(n) { + return this.value[n] + } + + write(n, value) { + this.#value[n] = value + } +} + +export class Rule { + constructor(from, to, predicate, replaceSymbol, action) { + this.fromState = from + this.toState = to + this.predicate = predicate + this.replaceSymbol = replaceSymbol + this.action = action + } + + static is(from, to, currentSymbol, nextSymbol, action) { + return new Rule( + from, + to, + Predicate.Equals(currentSymbol), + nextSymbol, + action + ) + } +} + +export class Rules { + #firstState + #rules = new Map() + + constructor(rules) { + this.#firstState = rules[0] + this.#initialize(rules) + } + + get firstState() { return this.#firstState } + + #initialize(rules) { + rules.forEach(rule => { + const transition = this.#rules.get(rule.fromState) + + if (transition) { + transition.push(rule) + } else { + this.#rules.set(rule.fromState, [rule]) + } + }) + } + + get(state, reader) { + return this.#rules.get(state)?.find(rule => rule.predicate(reader)) ?? false + } +} + +class ReaderState { + constructor(position, symbol) { + this.position = position + this.symbol = symbol + } + + static from(reader) { + return new ReaderState(reader.position, reader.symbol) + } +} + +class Iterator extends Array { + #index = 0 + + done() { + return this.length === 0 || this.#index === this.length - 1 + } + + peek() { + return this[this.#index] + } + + next() { + return this[this.#index++] + } + + take(n) { + const result = [] + while (!this.done() && n--) { + result.push(this.next()) + } + return result + } + + skip(n) { + this.take(n) + return this + } + + takeUntil(fn) { + const result = [] + let c = 0 + while (!this.done() && this.peek() && !fn(this.peek())) { + result.push(this.next()) + c++ + } + return result + } + + skipUntil(fn) { + this.takeUntil(fn) + return this + } +} + +export class Reader { + #head + #tape + #stack = [] + + constructor(head, tape) { + this.#head = head + this.#tape = tape + } + + get head() { return this.#head } + get tape() { return this.#tape } + get stack() { return Iterator.from(this.#stack) } + + get position() { + return this.#head.position + } + + get length() { + return this.#tape.length + } + + get symbol() { + return this.#head.read(this.#tape) + } + + clear() { + this.#stack.length = 0 + } + + inBounds(index) { + return index >= 0 && index <= this.length - 1 + } + + push(x = ReaderState.from(this)) { + this.#stack.push(x) + } + + pop() { + return this.#stack.pop() + } + + peek() { + return this.#stack[this.#stack.length - 1] + } +} + +const colors = { + brightWhite: [97, 107], + brightCyan: [96, 106], + brightMagenta: [95, 105], + brightBlue: [94, 104], + brightYellow: [93, 103], + brightGreen: [92, 102], + brightRed: [91, 101], + brightBlack: [90, 100], + white: [37, 47], + cyan: [36, 46], + magenta: [35, 45], + blue: [34, 44], + yellow: [33, 43], + green: [32, 42], + red: [31, 41], + black: [30, 40], +} + +const color = (code, t) => { + if (Array.isArray(code)) { + return code.reduce((acc, c) => color(c, acc), t) + } + + return `\x1b[${code}m${t}` + (t.endsWith(reset) ? '' : reset) +} +const reset = '\x1b[0m' +//const color = t => `\x1b[44m\x1b[93m${t}\x1b[0m` +const highlight = (i, arr, col = colors.cyan, spaces = 0) => { + const a = arr.slice() + const len = a[i].length + a[i] = color(col, a[i]) + const diff = a[i].length - len + //console.log(a[i].length, len, diff, spaces) + a[i] = a[i].padEnd(spaces + diff) + return a +} +const range = n => [...Array(n).keys()] + +export class TuringMachine { + #head = new Head() + #rules + #state + #reader + + constructor(tape, rules, initialState = rules.firstState.fromState, debug = false) { + this.tape = tape + this.debug = debug + + this.#rules = rules + this.#state = initialState + + this.#reader = new Reader(this.#head, this.tape) + } + + get state() { return this.#state } + get value() { return this.tape.value } + + display(rule, next) { + console.log(this.#state, '->', rule.toState, '(', this.#reader.symbol, '->', next, ')') + const length = this.tape.length + const spaces = length.toString().length + 1 + console.log(highlight(this.#reader.position, this.value, [1, 2, 4, colors.black[1], colors.yellow[0]], spaces).join(' '.repeat(spaces))) + const numbers = range(this.tape.length).map(String) + const highlighted = highlight(this.#reader.position, numbers, [1, 7], spaces + 3).map(x => x.padEnd(spaces + 1, ' ')).join('') + console.log(highlighted) + } + + step(rule) { + const next = rule.replaceSymbol(this.#reader) + + if (this.debug) { + this.display(rule, next) + } + + if (next) { + this.#head.write(this.tape, next) + } + this.#state = rule.toState + this.#head.move(rule.action(this.#reader)) + } + + next() { + return this.#rules.get(this.#state, this.#reader) + } + + exec() { + let next + while (next = this.next()) { + this.step(next) + } + } +} + + diff --git a/nine/src/index.js b/nine/src/index.js new file mode 100644 index 0000000..b80bc26 --- /dev/null +++ b/nine/src/index.js @@ -0,0 +1,143 @@ +import { readFile } from 'node:fs/promises' +import { resolve } from 'node:path' +import { Rules, Rule, Predicate, Replace, Tape, TuringMachine, Seek } from './automaton.js' + +const { First, Last, Left, Right, Stay, SeekFromStack, JumpFromStack } = Seek +const { ClearStack, Nothing, Push, Seq, Swap, With } = Replace +const { InBounds, Not, OutOfBounds, True } = Predicate + +const State = Object.freeze({ + Start: 'start', + ScanRight: 'scan-right', + FindFile: 'find-file', + ScanLeft: 'scan-left', + FindSpace: 'find-space', + ClearFile: 'clear-file', +}) + +const IsFreeSpace = Predicate.Equals('.') +const IsNotFree = Not(IsFreeSpace) +const IsFartherRight = x => x.peek().position <= x.position + +const dot = ch => ch.symbol === '.' +const notDot = ch => !dot(ch) +const TrySkipFiles = x => { + const stack = x.stack.slice() + const a = stack.skipUntil(dot).takeUntil(notDot) + const skip = !stack.done() + //console.log(a, 'skipping?', skip, !stack.done()) + return skip +} + +const FirstOrStack = x => { + // unwind the stack + const states = [] + let next + while (next = x.pop()) { + if (next.position < x.position) { + break + } else { + states.push(next) + next = null + } + } + + + // rewind it + states.reverse() + let tmp + while (tmp = states.pop()) { + x.push(tmp) + } + + return next ? next : First(x) + //return x.peek() ? x.pop().position : First(x) +} + +const TrySkipping = Seq(InBounds(Right), TrySkipFiles) + +const HasMoreFiles = x => { + const stack = x.stack.slice() + stack.skipUntil(dot).skipUntil(notDot) + return !stack.done() +} + +const ShouldContinue = Predicate.Every(OutOfBounds(Right), HasMoreFiles) + +// | From | To | Condition | Next Character | Move Direction | +// | ------------------------------------------------------------------------------| +// new Rule('q0', 'q1', reader => reader.symbol === '1', (_reader) => '0', 1) + +const rules = new Rules([ + new Rule(State.Start, State.ScanRight, True, Nothing, Right), + new Rule(State.ScanRight, State.FindFile, TrySkipping, ClearStack, Last), + new Rule(State.ScanRight, State.FindFile, ShouldContinue, ClearStack, Stay), + new Rule(State.ScanRight, State.ScanRight, InBounds(Right), Push, Right), + new Rule(State.FindFile, State.ScanLeft, IsNotFree, Seq(ClearStack, Push), Stay), + new Rule(State.FindFile, State.FindFile, IsFreeSpace, Nothing, Left), + new Rule(State.ScanLeft, State.ScanLeft, InBounds(Left), Nothing, FirstOrStack), + new Rule(State.ScanLeft, State.FindSpace, OutOfBounds(Left), Nothing, Stay), + new Rule(State.FindSpace, State.ScanRight, IsFartherRight, ClearStack, Right), + new Rule(State.FindSpace, State.ClearFile, IsFreeSpace, Swap, JumpFromStack), + new Rule(State.FindSpace, State.FindSpace, InBounds(Right), Nothing, Right), + new Rule(State.ClearFile, State.ScanRight, True, With('.'), SeekFromStack(false)), +]) + +const char = x => String.fromCodePoint(x) +const toInt = x => x.codePointAt() - 32 +const repeat = (n, value) => value.repeat(n) + +const expand = input => { + return input.split('').reduce((acc, n, i) => { + if (i % 2) { + return acc.concat(repeat(n, char(46))) + } else { + return acc.concat(repeat(n, char((i / 2) + 32))) + } + }, '') +} + +const compress = input => { + const tape = new Tape(input) + const tm = new TuringMachine(tape, rules) + tm.debug = false + tm.exec() + return tm.value +} + +const notFree = ch => ch !== '.' +const mul = (a, b) => a * b +const add = (a, b) => a + b + +const checksum = input => input.filter(notFree).map(toInt).map(mul).reduce(add) + +const main = input => { + const expanded = expand(input) + console.log(expanded) + const compressed = compress(expanded) + console.log(compressed.filter(notFree).map(toInt)) + const sum = checksum(compressed) + return sum +} + +const tests = [ + ['2333133121414131402', 1928], + ['1010101010101010101010', 385], + ['111111111111111111111', 290], + ['10101010101010101010101', 506], + ['90909', 486] +] + +tests.forEach(([input, expected]) => { + console.log(`input length is ${input.length}`) + const result = main(input) + console.log(result === expected, result) +}) + +//readFile(resolve('./input'), { encoding: 'utf8' }) +// .then(x => { +// console.log(`input length is ${x.length}`) +// return main(x) +// }) +// .then(console.log) +