18 files changed
+1487
-269
No files matched your search
@@ -0,0 +1,30 @@
|
||||
# 1.2.0
|
||||
|
||||
- core
|
||||
- async.ts
|
||||
- wait: Promise that resolves after a timeout
|
||||
- EventStream: Moved EventStream from iterable.ts to async.ts
|
||||
- pinkyPromise: Moved pinkyPromise from iterable.ts to async.ts
|
||||
- iterable.ts
|
||||
- Fixed range not working with a reversed range
|
||||
- Deprecated choose and shuffle, use the methods on the Random class in rng.ts instead!
|
||||
- EventStream: Moved EventStream from iterable.ts to async.ts
|
||||
- pinkyPromise: Moqved pinkyPromise from iterable.ts to async.ts
|
||||
- rng.ts
|
||||
- Added RNG.chooseWithWeights
|
||||
- string.ts
|
||||
- Sorted stuff from helpers.ts into here
|
||||
- tools
|
||||
- structures.ts
|
||||
- Common computer science data structures, such as a stack and queue
|
||||
- graph
|
||||
- Graph implementation, as well as a few common graph theory algorithms
|
||||
- math/primes.ts
|
||||
- Functions to to help use with prime numbers
|
||||
|
||||
# 1.1.0
|
||||
|
||||
- Add limit and limitAsync by @MindfulMinun in #1
|
||||
- Add rng.ts by @MindfulMinun in #2
|
||||
|
||||
# 1.0.0 (Initial release)
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Promise that resolves after timeout, optionally with a value to resolve with.
|
||||
*
|
||||
* @example
|
||||
* // Play hide and seek, count to 100 (seconds)
|
||||
* // Both methods are identical
|
||||
* wait(100e3).then(() => "Ready or not, here I come!").then(seek)
|
||||
* wait(100e3, "Ready or not, here I come!").then(seek)
|
||||
* @since 2020-06-23
|
||||
*/
|
||||
export function wait(timeout: number): Promise<void>
|
||||
export function wait<T>(timeout: number, resolveWith: T): Promise<T>
|
||||
export function wait<T>(timeout: number, resolveWith?: T): Promise<T | void> {
|
||||
return new Promise(resolve => setTimeout(() => resolve(resolveWith), timeout))
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to handle events and turn them into async iterables
|
||||
*
|
||||
* ```ts
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
*
|
||||
* document.body.addEventListener('click', ev => stream.emit(ev))
|
||||
* wait(10e3).then(() => stream.end())
|
||||
*
|
||||
* for await (const event of stream) {
|
||||
* alert("Clicked!")
|
||||
* }
|
||||
* ```
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
export class EventStream<T> implements AsyncIterable<T> {
|
||||
#done: boolean
|
||||
#events: T[]
|
||||
#resolve: () => void
|
||||
#promise!: Promise<void>
|
||||
|
||||
constructor() {
|
||||
this.#done = false
|
||||
this.#events = []
|
||||
this.#resolve = () => { }
|
||||
this.#defer()
|
||||
}
|
||||
|
||||
#defer() {
|
||||
this.#promise = new Promise(r => this.#resolve = r)
|
||||
}
|
||||
|
||||
async*[Symbol.asyncIterator]() {
|
||||
while (!this.#done) {
|
||||
await this.#promise
|
||||
yield* this.#events
|
||||
this.#events = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event. For-await-of listeners of this class instance will recieve this event.
|
||||
* Note that once an event is emitted, it cannot be cancelled.
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
emit(event: T) {
|
||||
this.#events.push(event)
|
||||
this.#resolve()
|
||||
this.#defer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a specific event to be emitted according to a predicate.
|
||||
*
|
||||
* @example
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
* // Some events happen...
|
||||
* const click = stream.once(ev => ev.type === 'click')
|
||||
* @since 2021-12-22
|
||||
*/
|
||||
once(predicate: (value: T) => boolean) {
|
||||
return once(this, predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the iterator. This terminates for-await-of loops listening for events,
|
||||
* and any newly dispatched events will not be sent to the iterator.
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
end() {
|
||||
this.#done = true
|
||||
this.#resolve()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any async iterable to yield a specific value according to a predicate.
|
||||
*
|
||||
* @example
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
* // Some events happen...
|
||||
* const click = once(stream, ev => ev.type === 'click')
|
||||
* @since 2021-12-22
|
||||
*/
|
||||
export async function once<T>(source: AsyncIterable<T>, predicate: (value: T) => boolean) {
|
||||
for await (const value of source) {
|
||||
if (predicate(value)) return value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A "destructured" Promise, useful for waiting for callbacks.
|
||||
* @example
|
||||
* // When awaited, this function returns a promise that resolves to
|
||||
* // an *OPEN* WebSocket
|
||||
* () => {
|
||||
* const sock = new WebSocket('wss://wss.example.com')
|
||||
* const [p, res, rej] = pinkyPromise<typeof sock>()
|
||||
* sock.onerror = err => rej(err)
|
||||
* sock.onopen = () => res(sock)
|
||||
* return p
|
||||
* }
|
||||
*
|
||||
* @author MindfulMinun
|
||||
* @since 2022-06-05
|
||||
*/
|
||||
export function pinkyPromise(): [Promise<void>, () => void, (reason?: unknown) => void]
|
||||
export function pinkyPromise<T>(): [Promise<T>, (value: T) => void, (reason?: unknown) => void]
|
||||
export function pinkyPromise<T>(): [
|
||||
Promise<T | void>,
|
||||
(value?: T | PromiseLike<T>) => void,
|
||||
(reason?: unknown) => void
|
||||
] {
|
||||
let resolve: (value?: T | PromiseLike<T>) => void
|
||||
let reject: (error: unknown) => void
|
||||
|
||||
// This works because callbacks to the Promise constructor are called synchronously.
|
||||
const p = new Promise<T | void>((yay, nay) => {
|
||||
resolve = value => yay(value)
|
||||
reject = reason => nay(reason)
|
||||
})
|
||||
|
||||
// HACK: This works because callbacks to the Promise constructor are called synchronously.
|
||||
// In other words, they're immediately invoked, so they're available immediately after
|
||||
// the assignment to `p`
|
||||
return [p, resolve!, reject!]
|
||||
}
|
||||
+80
-10
@@ -1,5 +1,5 @@
|
||||
/// <reference lib="dom" />
|
||||
import { templateNoop, HTML_ESCAPES, xss } from "./helpers.ts"
|
||||
import { templateNoop, HTML_ESCAPES, xss } from "./string.ts"
|
||||
|
||||
export { HTML_ESCAPES }
|
||||
|
||||
@@ -17,6 +17,7 @@ export { HTML_ESCAPES }
|
||||
* ${html`<em>Look ma, nested HTML!</em>`}
|
||||
* ${`<img src="/bogus/path" onerror="alert('xss')">`}
|
||||
* </strong>`
|
||||
*
|
||||
* // DocumentFragment
|
||||
* // └─ strong
|
||||
* // ├─ em
|
||||
@@ -27,8 +28,8 @@ export { HTML_ESCAPES }
|
||||
* @since 2020-06-23
|
||||
*/
|
||||
export function html(content: string): DocumentFragment
|
||||
export function html(strings: TemplateStringsArray, ...exprs: any[]): DocumentFragment
|
||||
export function html(strings: TemplateStringsArray | string, ...exprs: any[]): DocumentFragment {
|
||||
export function html(strings: TemplateStringsArray, ...exprs: unknown[]): DocumentFragment
|
||||
export function html(strings: TemplateStringsArray | string, ...exprs: unknown[]): DocumentFragment {
|
||||
const temp = document.createElement("template")
|
||||
let out = ""
|
||||
|
||||
@@ -45,7 +46,7 @@ export function html(strings: TemplateStringsArray | string, ...exprs: any[]): D
|
||||
continue
|
||||
}
|
||||
// Otherwise, just join the expressions together, escaping dynamic content
|
||||
out += strings[i] + (exprs[i] != null ? xss("" + exprs[i]) : "")
|
||||
out += strings[i] + (typeof exprs[i] != 'undefined' ? xss("" + exprs[i]) : "")
|
||||
}
|
||||
temp.innerHTML = out
|
||||
const clone = temp.content.cloneNode(true) as DocumentFragment
|
||||
@@ -55,7 +56,7 @@ export function html(strings: TemplateStringsArray | string, ...exprs: any[]): D
|
||||
if (!(node instanceof HTMLElement)) return
|
||||
if (!node.parentNode) return
|
||||
const index = parseInt(node.getAttribute("data-replaceindex") || "", 10)
|
||||
node.parentNode.replaceChild(exprs[index], node);
|
||||
node.parentNode.replaceChild(exprs[index] as Node, node);
|
||||
})
|
||||
|
||||
return clone
|
||||
@@ -67,18 +68,87 @@ export function html(strings: TemplateStringsArray | string, ...exprs: any[]): D
|
||||
* @since 2022-06-04
|
||||
*/
|
||||
export function textNode(templ: string): Text
|
||||
export function textNode(templ: TemplateStringsArray, ...values: any[]): Text
|
||||
export function textNode(templ: string | TemplateStringsArray, ...values: any[]): Text {
|
||||
export function textNode(templ: TemplateStringsArray, ...values: unknown[]): Text
|
||||
export function textNode(templ: string | TemplateStringsArray, ...values: unknown[]): Text {
|
||||
const string = templateNoop(templ, ...values)
|
||||
return document.createTextNode(string)
|
||||
}
|
||||
|
||||
/**
|
||||
* FLIP is a mnemonic device for effective JavaScript animations: First, Last, Invert, Play.
|
||||
* This helper class makes it easy to perform FLIP animations.
|
||||
*
|
||||
* https://aerotwist.com/blog/flip-your-animations/
|
||||
*
|
||||
* @author MindfulMinun
|
||||
* @since 2022-12-31
|
||||
*/
|
||||
class _FlipAnimator {
|
||||
el: Element
|
||||
#first: DOMRect | null = null
|
||||
|
||||
constructor(el: Element) {
|
||||
this.el = el
|
||||
this.#first = null
|
||||
}
|
||||
|
||||
/**
|
||||
* This method immediately performs a FLIP animation.
|
||||
* The caller must pass in a callback that describes the transition between the first and last states.
|
||||
*/
|
||||
async flip(cb: (this: Element, el: Element, rect: DOMRect) => Promise<void> | void, options: KeyframeAnimationOptions = {}) {
|
||||
// Get the current state of the element
|
||||
this.recordFirstState()
|
||||
|
||||
// Call the callback. The callback should change the element's DOMRect.
|
||||
await cb.call(this.el, this.el, this.#first!)
|
||||
|
||||
// Animate the element from the first state to the last state
|
||||
return this.animateFromFirst(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures the initial DOMRect of the element.
|
||||
* The caller may also pass in a DOMRect to use instead of querying the DOM.
|
||||
*/
|
||||
recordFirstState(rect: DOMRect = this.el.getBoundingClientRect()) {
|
||||
this.#first = rect
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Using the captured first state, this method will animate the element
|
||||
* from the first state to the last state.
|
||||
* Then this method will clear the DOMRects.
|
||||
*/
|
||||
animateFromFirst(options: KeyframeAnimationOptions = {}) {
|
||||
if (!this.#first) throw new Error('No first state recorded.')
|
||||
const last = this.el.getBoundingClientRect()
|
||||
|
||||
const deltaX = this.#first.left - last.left
|
||||
const deltaY = this.#first.top - last.top
|
||||
const scaleX = this.#first.width / last.width
|
||||
const scaleY = this.#first.height / last.height
|
||||
|
||||
this.#first = null
|
||||
|
||||
return this.el.animate([
|
||||
{ transform: `translate(${deltaX}px, ${deltaY}px) scale(${scaleX}, ${scaleY})` },
|
||||
{ transform: 'none' }
|
||||
], {
|
||||
duration: 300,
|
||||
easing: 'ease-in-out',
|
||||
...options
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the DOM recursively, calling the callback for each node.
|
||||
* @deprecated
|
||||
* @param {Node} root The root of the DOM walk
|
||||
* @param {number} [filters] A bitmask of what nodes to show. Defaults to `NodeFilter.SHOW_ELEMENT`.
|
||||
* @param {(node: Node, root: Node, walker: TreeWalker) => boolean} callback If this callback returns true, stop tree traversal.
|
||||
* @param root - The root of the DOM walk
|
||||
* @param callback - If this callback returns true, stop tree traversal.
|
||||
* @param filters - A bitmask of what nodes to show. Defaults to `NodeFilter.SHOW_ELEMENT`.
|
||||
* @author MindfulMinun
|
||||
* @since 2020-06-29
|
||||
*/
|
||||
+41
-142
@@ -1,24 +1,13 @@
|
||||
/**
|
||||
* Represents a tuple, or a homogenous array of a fixed length.
|
||||
* Represents a homogenous array of a fixed length.
|
||||
*
|
||||
* const deck: Tuple<Card, 52> = [Card, Card, Card, ...];
|
||||
* const deck: Repeated<Card, 52> = [Card, Card, Card, ...];
|
||||
*/
|
||||
export type Tuple<
|
||||
T,
|
||||
N extends number,
|
||||
R extends readonly T[] = [],
|
||||
> = R['length'] extends N ? R : Tuple<T, N, readonly [T, ...R]>;
|
||||
|
||||
/**
|
||||
* Chooses a random element from an array
|
||||
*
|
||||
* choose(document.all).click()
|
||||
* @since 2020-06-29
|
||||
*/
|
||||
export function choose<T>(arr: T[]): T {
|
||||
if (!arr.length) throw new Error('Cannot choose from an empty array')
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
export type Repeated<
|
||||
T,
|
||||
N extends number,
|
||||
R extends readonly T[] = [],
|
||||
> = R['length'] extends N ? R : Repeated<T, N, readonly [T, ...R]>;
|
||||
|
||||
/**
|
||||
* Creates an iterable that always yields the given value.
|
||||
@@ -31,7 +20,18 @@ export function* fill<T>(value = 0 as unknown as T, length = Infinity): Generato
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an iterable that yields numbers between a range.
|
||||
* A ganerator that yields numbers between a range, akin to Python's {@link https://docs.python.org/3.11/library/stdtypes.html#ranges range}.
|
||||
* If `start` is greater than `end`, the range will decrement accordingly
|
||||
*
|
||||
* @example
|
||||
* for (const i of range(10)) {
|
||||
* console.log(i) // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
|
||||
* }
|
||||
* range(6) // -> 0, 1, 2, 3, 4, 5
|
||||
* range(3, 9) // -> 3, 4, 5, 6, 7, 8
|
||||
* range(9, 3) // -> 9, 8, 7, 6, 5, 4
|
||||
* range(0, 10, 2) // -> 0, 2, 4, 6, 8
|
||||
* range(10, 0, 2) // -> 10, 8, 6, 4, 2
|
||||
* @author MindfulMinun
|
||||
* @since 2022-04-27
|
||||
*/
|
||||
@@ -43,8 +43,11 @@ export function* range(a: number, b?: number, c?: number) {
|
||||
typeof b === "undefined" ? [0, a, 1] :
|
||||
typeof c === "undefined" ? [a, b, 1] : [a, b, Math.abs(c)]
|
||||
)
|
||||
const delta = end < start ? -step : step
|
||||
for (let i = start; i < end; i += delta)
|
||||
|
||||
const positive = start < end
|
||||
const delta = positive ? step : -step
|
||||
|
||||
for (let i = start; positive ? i < end : end < i; i += delta)
|
||||
yield i
|
||||
}
|
||||
|
||||
@@ -61,7 +64,7 @@ export function* limit<T>(count: number, iterable: Iterable<T>) {
|
||||
i++
|
||||
} else break
|
||||
}
|
||||
return iterable
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +80,7 @@ export async function* limitAsync<T>(count: number, iterable: AsyncIterable<T>)
|
||||
i++
|
||||
} else break
|
||||
}
|
||||
return iterable
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +122,21 @@ export function divide<T>(list: T[], predicate: (this: typeof list, value: T, in
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the `choose` method on the `Random` class from `core/rng.ts#Random` instead!
|
||||
*
|
||||
* Chooses a random element from an array
|
||||
*
|
||||
* choose(document.all).click()
|
||||
* @since 2020-06-29
|
||||
*/
|
||||
export function choose<T>(arr: T[]): T {
|
||||
if (!arr.length) throw new Error('Cannot choose from an empty array')
|
||||
return arr[Math.floor(Math.random() * arr.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the `shuffle` method on the `Random` class from `core/rng.ts#Random` instead!
|
||||
*
|
||||
* Shuffles an array. Note that this function swaps the elements
|
||||
* *in place*, meaning the original array is modified.
|
||||
* @since 2020-07-19
|
||||
@@ -142,122 +160,3 @@ export function swap<T>(arr: T[], i: number, j: number): void {
|
||||
arr[i] = arr[j]
|
||||
arr[j] = carry
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to handle events and turn them into async iterables
|
||||
*
|
||||
* ```ts
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
*
|
||||
* document.body.addEventListener('click', ev => stream.emit(ev))
|
||||
* wait(10e3).then(() => stream.end())
|
||||
*
|
||||
* for await (const event of stream) {
|
||||
* alert("Clicked!")
|
||||
* }
|
||||
* ```
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
export class EventStream<T> {
|
||||
#done: boolean
|
||||
#events: T[]
|
||||
#resolve: () => void
|
||||
#promise!: Promise<void>
|
||||
|
||||
constructor() {
|
||||
this.#done = false
|
||||
this.#events = []
|
||||
this.#resolve = () => { }
|
||||
this.#defer()
|
||||
}
|
||||
|
||||
#defer() {
|
||||
this.#promise = new Promise(r => this.#resolve = r)
|
||||
}
|
||||
|
||||
async*[Symbol.asyncIterator]() {
|
||||
while (!this.#done) {
|
||||
await this.#promise
|
||||
yield* this.#events
|
||||
this.#events = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event. For-await-of listeners of this class instance will recieve this event.
|
||||
* Note that once an event is emitted, it cannot be cancelled.
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
emit(event: T) {
|
||||
this.#events.push(event)
|
||||
this.#resolve()
|
||||
this.#defer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a specific event to be emitted according to a predicate.
|
||||
*
|
||||
* @example
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
* // Some events happen...
|
||||
* const click = stream.once(ev => ev.type === 'click')
|
||||
* @since 2021-12-22
|
||||
*/
|
||||
once(predicate: (value: T) => boolean) {
|
||||
return once(this, predicate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the iterator. This terminates for-await-of loops listening for events,
|
||||
* and any newly dispatched events will not be sent to the iterator.
|
||||
* @since 2021-08-07
|
||||
*/
|
||||
end() {
|
||||
this.#done = true
|
||||
this.#resolve()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any async iterable to yield a specific value according to a predicate.
|
||||
*
|
||||
* @example
|
||||
* const stream = new EventStream<MouseEvent>()
|
||||
* // Some events happen...
|
||||
* const click = once(stream, ev => ev.type === 'click')
|
||||
* @since 2021-12-22
|
||||
*/
|
||||
export async function once<T>(source: AsyncIterable<T>, predicate: (value: T) => boolean) {
|
||||
for await (const value of source) {
|
||||
if (predicate(value)) return value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A "destructured" Promise, useful for waiting for callbacks.
|
||||
* @example
|
||||
* // When awaited, this function returns a promise that resolves to
|
||||
* // an *OPEN* WebSocket
|
||||
* () => {
|
||||
* const sock = new WebSocket()
|
||||
* const [p, res, rej] = pinkyPromise()
|
||||
* sock.onerror = err => rej(err)
|
||||
* sock.onopen = () => res(sock)
|
||||
* return p
|
||||
* }
|
||||
*
|
||||
* @author MindfulMinun
|
||||
* @since 2022-06-05
|
||||
*/
|
||||
export function pinkyPromise<T>(): [Promise<T>, (value: T) => void, (error: unknown) => unknown] {
|
||||
let resolve: (value: T) => void
|
||||
let reject: (error: unknown) => void
|
||||
|
||||
// This works because callbacks to the Promise constructor are called synchronously.
|
||||
const p = new Promise<T>((yay, nay) => {
|
||||
resolve = yay
|
||||
reject = nay
|
||||
})
|
||||
|
||||
return [p, resolve!, reject!]
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
console.error("Hey there, past me! Please don't import <https://mirai.benjic.xyz/lib/mod.ts>, instead import specific files please!")
|
||||
console.error("Hey there, past me! Please don't import `mod.ts`, instead import specific files!")
|
||||
|
||||
// export * from "./array.ts"
|
||||
// export * from "./helpers.ts"
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
import { swap } from './iterable.ts'
|
||||
|
||||
/**
|
||||
* The base class for creating a (potentially seeded) random number generator.
|
||||
* Provides a plethora of methods for manipulating objects with randomness.
|
||||
* (I predicted you might need them, you are not random)
|
||||
*
|
||||
* Take a look at [this list of JavaScript PRNGs](https://github.com/bryc/code/blob/master/jshash/PRNGs.md)
|
||||
* for guidance on implementation.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-08
|
||||
*/
|
||||
export abstract class RNG implements Iterable<number> {
|
||||
/**
|
||||
* This is the main number generation function. It should return a number
|
||||
* between 0 (inclusive) and 1 (exclusive) as well as update its internal state.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-16
|
||||
*/
|
||||
abstract nextValue(): number
|
||||
|
||||
/**
|
||||
* Yields numbers between zero and one
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-16
|
||||
*/
|
||||
*[Symbol.iterator]() {
|
||||
while (true) yield this.float()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a floating point number between 0 (inclusive) and 1 (exclusive),
|
||||
* using the algorithm implemented in {@link nextValue()}
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-16
|
||||
*/
|
||||
float() {
|
||||
return this.nextValue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an integer between the provided bounds. This returns integers *up to* `upper`, without including it.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-16
|
||||
*/
|
||||
int(upper: number): number
|
||||
int(lower: number, upper: number): number
|
||||
int(a: number, b?: number): number {
|
||||
const [min, max] = typeof b === "undefined" ? [0, a] : [a, b]
|
||||
return Math.floor(this.float() * (max - min + 1) + min)
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses a random element from a provided array
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-16
|
||||
*/
|
||||
choose<T>(arr: T[]) {
|
||||
return arr[Math.floor(this.float() * arr.length)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles an array. Note that this function swaps the elements
|
||||
* *in place*, meaning the original array is modified.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-16
|
||||
*/
|
||||
shuffle<T>(arr: T[]) {
|
||||
let i, j
|
||||
for (i = arr.length - 1; i > 0; i--) {
|
||||
j = Math.floor(this.float() * (i + 1))
|
||||
swap(arr, i, j)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses an element from an array randomly, but with weighted probabilities.
|
||||
* @param weights - An array of numbers. `weights[i]` corresponds to the probability
|
||||
* of `items[i]` being chosen. The weights can be any nonnegative number, decimal or otherwise,
|
||||
* and the sum of weights *does not* have to be 1.
|
||||
* @example
|
||||
* // 'paper' will be returned about half of the time, and 'rock' and 'scissors'
|
||||
* // will be returned about a 1/4th of the time each.
|
||||
* BaseClass.chooseWithWeights(
|
||||
* ['rock', 'paper', 'scissors'],
|
||||
* [1, 2, 1]
|
||||
* )
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-16
|
||||
*/
|
||||
chooseWithWeights<T>(items: T[], weights: number[]) {
|
||||
if (items.length != weights.length) throw Error(`Expected items and weights to have the same number of elements, but items has ${items.length} elements, and weights has ${weights.length}.`)
|
||||
|
||||
const sumOfWeights = weights.reduce((acc, v) => acc + v)
|
||||
const picked = this.float() * sumOfWeights
|
||||
|
||||
let i = 0
|
||||
let currentSum = 0
|
||||
|
||||
for (; i < weights.length - 1; i++) {
|
||||
currentSum += weights[i]
|
||||
if (currentSum > picked) break
|
||||
}
|
||||
|
||||
return items[i]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple RNG which returns numbers generated by {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random Math.random()}
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-08
|
||||
*/
|
||||
export class Random extends RNG {
|
||||
nextValue() { return Math.random() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A seeded RNG which implements Mulberry32
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-08
|
||||
*/
|
||||
export class Mulberry32 extends RNG {
|
||||
#seed!: number
|
||||
originalSeed!: number
|
||||
|
||||
constructor(seed = Date.now()) {
|
||||
super()
|
||||
this.resetSeed(seed)
|
||||
}
|
||||
|
||||
/** Resets the seed */
|
||||
resetSeed(seed = this.#seed) {
|
||||
this.originalSeed = seed
|
||||
this.#seed = seed
|
||||
}
|
||||
|
||||
nextValue() {
|
||||
// Mulberry32
|
||||
let a = this.#seed
|
||||
a |= 0
|
||||
a = a + 0x6D2B79F5 | 0
|
||||
let t = Math.imul(a ^ a >>> 15, 1 | a)
|
||||
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t
|
||||
this.#seed = a
|
||||
return ((t ^ t >>> 14) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -7,8 +7,8 @@ export const HTML_ESCAPES = {
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
"/": "/"
|
||||
}
|
||||
"/": "/",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Removes indentation from multiline strings.
|
||||
@@ -75,26 +75,13 @@ export function templateNoop(
|
||||
templ: TemplateStringsArray | string,
|
||||
...values: unknown[]
|
||||
): string {
|
||||
let strings = Array.from(typeof templ === 'string' ? [templ] : templ)
|
||||
return strings.reduce((result, currentString, i) =>
|
||||
result + currentString + (values[i] != null ? values[i] : ''),
|
||||
if (typeof templ === 'string') return templ
|
||||
return templ.reduce((result, currentString, i) =>
|
||||
result + currentString + (typeof values[i] !== 'undefined' ? values[i] : ''),
|
||||
'' // Start with the empty string
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Promise that resolves after timeout
|
||||
*
|
||||
* ```js
|
||||
* // Play hide and seek, count to 100 (seconds)
|
||||
* wait(100e3).then(() => "Ready or not, here I come!").then(seek)
|
||||
* ```
|
||||
* @since 2020-06-23
|
||||
*/
|
||||
export function wait(timeout: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, timeout))
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes unsafe strings for use in HTML
|
||||
*
|
||||
@@ -105,5 +92,5 @@ export function wait(timeout: number): Promise<void> {
|
||||
* @since 2020-06-23
|
||||
*/
|
||||
export function xss(unsafe: string): string {
|
||||
return unsafe.replace(/[&<>"'\/]/g, (key) => HTML_ESCAPES[key as keyof typeof HTML_ESCAPES])
|
||||
return unsafe.replace(/[&<>"'\/]/g, key => HTML_ESCAPES[key as keyof typeof HTML_ESCAPES])
|
||||
}
|
||||
@@ -1 +1,14 @@
|
||||
# pear 🍐
|
||||
|
||||
|
||||
<!--
|
||||
Endgames:
|
||||
Matrix:
|
||||
Eigendecomposition, Singular Value Decomposition, LU Decomposition, QR Decomposition, Cholesky Decomposition
|
||||
Dynamic Programming:
|
||||
Edit distance, Longest Common Subsquence
|
||||
Graphs:
|
||||
Dijkstra's, Prim, Kruskal
|
||||
Bellman-Ford, Floyd-Warshall
|
||||
Ford-Fulkerson
|
||||
-->
|
||||
@@ -1,94 +0,0 @@
|
||||
import { swap } from './iterable.ts'
|
||||
|
||||
/**
|
||||
* The base class for creating a (potentially seeded) random number generator.
|
||||
* Provides a plethora of methods for manipulating objects with randomness.
|
||||
* (I predicted you might need them, you are not random)
|
||||
*
|
||||
* Take a look at [this list of JavaScript PRNGs](https://github.com/bryc/code/blob/master/jshash/PRNGs.md)
|
||||
* for guidance on implementation.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-08
|
||||
*/
|
||||
export abstract class BaseRNG implements Iterable<number> {
|
||||
/**
|
||||
* This is the main number generation function.
|
||||
* It should return a number between 0 (inclusive) and 1 (exclusive) as well as update its internal state.
|
||||
*/
|
||||
abstract nextValue(): number
|
||||
|
||||
/** nyanyanya */
|
||||
*[Symbol.iterator]() {
|
||||
while (true) yield this.nextValue()
|
||||
}
|
||||
|
||||
/** Returns a floating point number between 0 (inclusive) and 1 (exclusive) */
|
||||
float() {
|
||||
return this.nextValue()
|
||||
}
|
||||
|
||||
/** Returns an integer between the provided bounds. This returns integers *up to* `upper`, without including it. */
|
||||
int(upper: number): number
|
||||
int(lower: number, upper: number): number
|
||||
int(a: number, b?: number): number {
|
||||
const rand = this.float()
|
||||
const [min, max] = typeof b === "undefined" ? [0, a] : [a, b]
|
||||
return Math.floor(rand * (max - min + 1) + min)
|
||||
}
|
||||
|
||||
/** Chooses a random element from a provided array */
|
||||
choose<T>(arr: T[]) {
|
||||
return arr[Math.floor(this.float() * arr.length)]
|
||||
}
|
||||
|
||||
/** Shuffles an array */
|
||||
shuffle<T>(arr: T[]) {
|
||||
let i, j
|
||||
for (i = arr.length - 1; i > 0; i--) {
|
||||
j = Math.floor(this.float() * (i + 1))
|
||||
swap(arr, i, j)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple RNG which returns numbers generated by {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math/random Math.random}
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-08
|
||||
*/
|
||||
export class Random extends BaseRNG {
|
||||
nextValue() { return Math.random() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A seeded RNG which implements Mulberry32
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-08
|
||||
*/
|
||||
export class Mulberry32 extends BaseRNG {
|
||||
#seed!: number
|
||||
originalSeed!: number
|
||||
|
||||
constructor(seed = Date.now()) {
|
||||
super()
|
||||
this.resetSeed(seed)
|
||||
}
|
||||
|
||||
/** Resets the seed */
|
||||
resetSeed(seed = this.#seed) {
|
||||
this.originalSeed = seed
|
||||
this.#seed = seed
|
||||
}
|
||||
|
||||
nextValue() {
|
||||
// Mulberry32
|
||||
let a = this.#seed
|
||||
a |= 0
|
||||
a = a + 0x6D2B79F5 | 0
|
||||
let t = Math.imul(a ^ a >>> 15, 1 | a)
|
||||
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t
|
||||
this.#seed = a
|
||||
return ((t ^ t >>> 14) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { g1, g2, g3 } from '../tools/graph/_graph-samples.ts'
|
||||
import { GraphSolver } from '../tools/graph/graph-solver.ts'
|
||||
import { assertEquals } from "https://deno.land/std/testing/asserts.ts"
|
||||
import { Path } from "../tools/graph/graph.ts"
|
||||
|
||||
// TODO: Add more tests!
|
||||
|
||||
Deno.test('GraphSolver::BFS w/ g1', () => {
|
||||
const G = g1()
|
||||
const solver = new GraphSolver(G)
|
||||
const path = solver.BFS(G.vertices.get('A')!, v => v.id === 'F')
|
||||
|
||||
const expectedV = ['A', 'C', 'F']
|
||||
const expectedE = ['AC', 'CF']
|
||||
|
||||
assertPathsEqual(path, expectedV, expectedE)
|
||||
})
|
||||
|
||||
Deno.test('GraphSolver::BFS w/ g2', () => {
|
||||
const G = g2()
|
||||
const solver = new GraphSolver(G)
|
||||
const path = solver.BFS(G.vertices.get('A')!, v => v.id === 'G')
|
||||
|
||||
const expectedV = ['A', 'C', 'G']
|
||||
const expectedE = ['AC', 'CG']
|
||||
|
||||
assertPathsEqual(path, expectedV, expectedE)
|
||||
})
|
||||
|
||||
Deno.test('GraphSolver::DFS w/ g1', () => {
|
||||
const G = g1()
|
||||
const solver = new GraphSolver(G)
|
||||
const path = solver.DFS(G.vertices.get('A')!, v => v.id === 'E')
|
||||
|
||||
assertPathsEqual(
|
||||
path,
|
||||
['A', 'C', 'F', 'E'],
|
||||
['AC', 'CF', 'EF']
|
||||
)
|
||||
})
|
||||
|
||||
Deno.test('GraphSolver::bidi Path', () => {
|
||||
const G = g3()
|
||||
const solver = new GraphSolver(G)
|
||||
const path = solver.bidi(G.vertices.get('A')!, G.vertices.get('E')!)
|
||||
|
||||
assertPathsEqual(
|
||||
path,
|
||||
'ABCDE'.split(''),
|
||||
'AB-BC-CD-DE'.split('-')
|
||||
)
|
||||
})
|
||||
|
||||
function assertPathsEqual<vData, eData>(
|
||||
path: Path<vData, eData> | null | undefined,
|
||||
expectedV: string[],
|
||||
expectedE: string[]
|
||||
) {
|
||||
if (!path) throw Error("Path is null or undefined")
|
||||
|
||||
try {
|
||||
assertEquals(path?.edges.length, expectedE.length)
|
||||
assertEquals(path?.vertices.length, expectedV.length)
|
||||
|
||||
expectedV.forEach((v, i) => assertEquals(path?.vertices[i].id, v))
|
||||
expectedE.forEach((e, i) => assertEquals(path?.edges[i].id, e))
|
||||
} catch (e) {
|
||||
throw Error(`Paths are different! Expected: [${expectedV.join(' -> ')}]; Actual: [${path.vertices.map(v => v.id).join(' -> ')}]`)
|
||||
}
|
||||
}
|
||||
+20
-3
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
choose, fill, range, limit
|
||||
} from "../iterable.ts"
|
||||
} from "../core/iterable.ts"
|
||||
|
||||
import { assertThrows, assertEquals, assert } from "https://deno.land/[email protected]/testing/asserts.ts"
|
||||
|
||||
@@ -29,7 +29,7 @@ Deno.test("range: one arg", () => {
|
||||
assert(iter.next().done)
|
||||
})
|
||||
|
||||
Deno.test("range: two args", () => {
|
||||
Deno.test("range: two args, positive", () => {
|
||||
const iter = range(40, 50)
|
||||
for (let i = 40; i < 50; i++) {
|
||||
const next = iter.next()
|
||||
@@ -38,7 +38,16 @@ Deno.test("range: two args", () => {
|
||||
assert(iter.next().done)
|
||||
})
|
||||
|
||||
Deno.test("range: three args", () => {
|
||||
Deno.test("range: two args, negative", () => {
|
||||
const iter = range(50, 40)
|
||||
for (let i = 50; i > 40; i--) {
|
||||
const next = iter.next()
|
||||
assertEquals(next.value, i)
|
||||
}
|
||||
assert(iter.next().done)
|
||||
})
|
||||
|
||||
Deno.test("range: three args, positive", () => {
|
||||
const iter = range(60, 80, 4)
|
||||
for (let i = 60; i < 80; i += 4) {
|
||||
const next = iter.next()
|
||||
@@ -47,6 +56,14 @@ Deno.test("range: three args", () => {
|
||||
assert(iter.next().done)
|
||||
})
|
||||
|
||||
Deno.test("range: three args, negative", () => {
|
||||
const iter = range(80, 60, 4)
|
||||
for (let i = 80; i > 60; i -= 4) {
|
||||
const next = iter.next()
|
||||
assertEquals(next.value, i)
|
||||
}
|
||||
assert(iter.next().done)
|
||||
})
|
||||
|
||||
Deno.test("limit: limits to the first n elements", () => {
|
||||
const values: number[] = []
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Graph } from './graph.ts'
|
||||
|
||||
// Box drawing characters!
|
||||
// ┌ ┬ ┐ ─│
|
||||
// ├ ┼ ┤ ╱╳╲
|
||||
// └ ┴ ┘ ╵╶╷╴
|
||||
|
||||
/**
|
||||
* Creates an *undirected cyclic graph*
|
||||
*
|
||||
* ```plaintext
|
||||
* ┌── B ─── D
|
||||
* │ │
|
||||
* A E ── F
|
||||
* │ │
|
||||
* └─ C ────┘
|
||||
* ```
|
||||
*/
|
||||
export function g1() {
|
||||
// A
|
||||
// / \
|
||||
// C B
|
||||
// \ / \
|
||||
// \ E D
|
||||
// \|
|
||||
// F
|
||||
const G = new Graph<null, null>({ directed: false })
|
||||
|
||||
const vertices = [
|
||||
G.createVertex(null, 'A'),
|
||||
G.createVertex(null, 'B'),
|
||||
G.createVertex(null, 'C'),
|
||||
G.createVertex(null, 'D'),
|
||||
G.createVertex(null, 'E'),
|
||||
G.createVertex(null, 'F')
|
||||
]
|
||||
|
||||
G.createEdge(vertices[0], vertices[1], null, { id: 'AB' })
|
||||
G.createEdge(vertices[0], vertices[2], null, { id: 'AC' })
|
||||
G.createEdge(vertices[1], vertices[3], null, { id: 'BD' })
|
||||
G.createEdge(vertices[1], vertices[4], null, { id: 'BE' })
|
||||
G.createEdge(vertices[2], vertices[5], null, { id: 'CF' })
|
||||
G.createEdge(vertices[3], vertices[5], null, { id: 'DF' })
|
||||
G.createEdge(vertices[4], vertices[5], null, { id: 'EF' })
|
||||
|
||||
return G
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an *undirected acyclic graph* for testing. Resembles a tree.
|
||||
*
|
||||
*
|
||||
* ```plaintext
|
||||
* A
|
||||
* / \
|
||||
* B C
|
||||
* / \ / \
|
||||
* D E F G
|
||||
* ```
|
||||
*/
|
||||
export function g2() {
|
||||
const G = new Graph<null, null>({ directed: false })
|
||||
|
||||
const vertices = [
|
||||
G.createVertex(null, 'A'),
|
||||
G.createVertex(null, 'B'),
|
||||
G.createVertex(null, 'C'),
|
||||
G.createVertex(null, 'D'),
|
||||
G.createVertex(null, 'E'),
|
||||
G.createVertex(null, 'F'),
|
||||
G.createVertex(null, 'G')
|
||||
]
|
||||
|
||||
G.createEdge(vertices[0], vertices[1], null, { id: 'AB' })
|
||||
G.createEdge(vertices[0], vertices[2], null, { id: 'AC' })
|
||||
G.createEdge(vertices[1], vertices[3], null, { id: 'BD' })
|
||||
G.createEdge(vertices[1], vertices[4], null, { id: 'BE' })
|
||||
G.createEdge(vertices[2], vertices[5], null, { id: 'CF' })
|
||||
G.createEdge(vertices[2], vertices[6], null, { id: 'CG' })
|
||||
|
||||
return G
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an *undirected weighted cyclic graph.* The numbers next to the edges are the weights.
|
||||
*
|
||||
* ```plaintext
|
||||
* 4 8 7 9
|
||||
* ┌──── B ───── C ───── D ────┐
|
||||
* │ │ 2│ ╲ │ │
|
||||
* A 11│ 7┌─── I ╲4 │14 E
|
||||
* │ │ ╱ 6│ ╲ │ │
|
||||
* └──── H ───── G ───── F ────┘
|
||||
* 8 1 2 10
|
||||
* ```
|
||||
*/
|
||||
export function g3() {
|
||||
const G = new Graph<null, number>({
|
||||
directed: false,
|
||||
weights: e => e.data
|
||||
})
|
||||
const vertices = [
|
||||
G.createVertex(null, 'A'),
|
||||
G.createVertex(null, 'B'),
|
||||
G.createVertex(null, 'C'),
|
||||
G.createVertex(null, 'D'),
|
||||
G.createVertex(null, 'E'),
|
||||
G.createVertex(null, 'F'),
|
||||
G.createVertex(null, 'G'),
|
||||
G.createVertex(null, 'H'),
|
||||
G.createVertex(null, 'I'),
|
||||
G.createVertex(null, 'J'),
|
||||
]
|
||||
|
||||
G.createEdge(vertices[0], vertices[1], 4, { id: 'AB' })
|
||||
G.createEdge(vertices[0], vertices[7], 8, { id: 'AH' })
|
||||
G.createEdge(vertices[1], vertices[2], 8, { id: 'BC' })
|
||||
G.createEdge(vertices[1], vertices[7], 11, { id: 'BH' })
|
||||
G.createEdge(vertices[2], vertices[3], 7, { id: 'CD' })
|
||||
G.createEdge(vertices[2], vertices[5], 4, { id: 'CF' })
|
||||
G.createEdge(vertices[2], vertices[8], 2, { id: 'CI' })
|
||||
G.createEdge(vertices[3], vertices[4], 9, { id: 'DE' })
|
||||
G.createEdge(vertices[3], vertices[5], 14, { id: 'DF' })
|
||||
G.createEdge(vertices[4], vertices[5], 10, { id: 'EF' })
|
||||
G.createEdge(vertices[5], vertices[6], 2, { id: 'FG' })
|
||||
G.createEdge(vertices[6], vertices[7], 1, { id: 'GH' })
|
||||
G.createEdge(vertices[6], vertices[8], 6, { id: 'GI' })
|
||||
|
||||
return G
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Graph, Vertex, VertexType, EdgeType, Path, PathType } from "./graph.ts"
|
||||
import { Queue, Stack } from "../structures.ts"
|
||||
|
||||
/**
|
||||
* Describes a search function for DFS and BFS.
|
||||
*/
|
||||
export type SearchFn<Graph> = (v: VertexType<Graph>, e: EdgeType<Graph> | null, pathmap: Map<typeof v, PathType<Graph>>) => boolean | void
|
||||
|
||||
/**
|
||||
* A graph solver, which can perform various algorithms on a graph.
|
||||
*/
|
||||
export class GraphSolver<vData, eData> {
|
||||
G: Graph<vData, eData>
|
||||
|
||||
constructor(G: Graph<vData, eData>) {
|
||||
this.G = G
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a breadth-first search on a graph. Returns the path from the root to the found node.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-10-23
|
||||
*/
|
||||
BFS(root: Vertex<vData, eData>, search: SearchFn<typeof this.G>): Path<vData, eData> | null {
|
||||
const queue = new Queue([root])
|
||||
const backPaths = new Map<Vertex<vData, eData>, Path<vData, eData>>()
|
||||
|
||||
const discovered = new WeakSet<typeof root>()
|
||||
discovered.add(root)
|
||||
|
||||
backPaths.set(root, this.G.createPath(root))
|
||||
|
||||
for (const v of queue) {
|
||||
const found = search(v, backPaths.get(v)?.edges.at(-1) ?? null, backPaths)
|
||||
if (found) return backPaths.get(v) ?? null
|
||||
|
||||
for (const E of v.adjacentEdges) {
|
||||
if (E.directed && v !== E.u) continue
|
||||
const w = E.not(v)
|
||||
|
||||
if (!discovered.has(w)) {
|
||||
discovered.add(w)
|
||||
const path = backPaths.get(v)!.copy()
|
||||
path.addEdge(E)
|
||||
backPaths.set(w, path)
|
||||
queue.enqueue(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we iterate through all vertices without finding one that fulfills `search`,
|
||||
// then the found vertex is null and we should return null
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a depth-first search on a graph. Returns the path from the root to the found node.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-09-28
|
||||
*/
|
||||
DFS(root: Vertex<vData, eData>, search: SearchFn<typeof this.G>): Path<vData, eData> | null {
|
||||
const stack = new Stack([root])
|
||||
const backPaths = new Map<VertexType<typeof this.G>, Path<vData, eData>>()
|
||||
|
||||
backPaths.set(root, this.G.createPath(root))
|
||||
|
||||
for (const v of stack) {
|
||||
const found = search(v, backPaths.get(v)?.edges.at(-1) ?? null, backPaths)
|
||||
if (found) return backPaths.get(v) ?? null
|
||||
|
||||
for (const E of v.adjacentEdges) {
|
||||
if (E.directed && v !== E.u) continue
|
||||
const w = E.not(v)
|
||||
stack.push(w)
|
||||
// Update the path!
|
||||
const path = backPaths.get(v)!.copy()
|
||||
path.addEdge(E)
|
||||
backPaths.set(w, path)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bidirectional search on a graph. Returns the shortest path from `left` to `right`.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-11-14
|
||||
*/
|
||||
bidi(left: Vertex<vData, eData>, right: Vertex<vData, eData>): Path<vData, eData> | null {
|
||||
const qL = new Queue([left])
|
||||
const qR = new Queue([right])
|
||||
|
||||
// Map a vertex to the path that led to it. Note that paths from the right
|
||||
// will be reversed so they can be concatenated with the paths from the left.
|
||||
const backL = new Map<Vertex<vData, eData>, Path<vData, eData>>()
|
||||
const backR = new Map<Vertex<vData, eData>, Path<vData, eData>>()
|
||||
|
||||
backL.set(left, this.G.createPath(left))
|
||||
backR.set(right, this.G.createPath(right))
|
||||
|
||||
const seenL = new Set<Vertex<vData, eData>>([left])
|
||||
const seenR = new Set<Vertex<vData, eData>>([right])
|
||||
|
||||
while (qL.length && qR.length) {
|
||||
const l = qL.dequeue()!
|
||||
const r = qR.dequeue()!
|
||||
|
||||
// The left side will traverse the graph normally,
|
||||
// following the direction of the arrows in the edges
|
||||
for (const E of l.adjacentEdges) {
|
||||
if (E.directed && l !== E.u) continue
|
||||
const w = E.not(l)
|
||||
if (!seenL.has(w)) {
|
||||
seenL.add(w)
|
||||
const path = backL.get(l)!.copy()
|
||||
path.addEdge(E)
|
||||
backL.set(w, path)
|
||||
qL.enqueue(w)
|
||||
}
|
||||
}
|
||||
|
||||
// For the right side, we will traverse the graph backwards
|
||||
// So, against the direction of the arrows :)
|
||||
for (const E of r.adjacentEdges) {
|
||||
// Compare against v since edges always point to v,
|
||||
// Edge u -> v
|
||||
if (E.directed && r !== E.v) continue
|
||||
const w = E.not(r)
|
||||
if (!seenR.has(w)) {
|
||||
seenR.add(w)
|
||||
const path = backR.get(r)!.copy()
|
||||
path.addEdge(E)
|
||||
backR.set(w, path)
|
||||
qR.enqueue(w)
|
||||
}
|
||||
}
|
||||
|
||||
// If we find a vertex that has been seen from both sides, then we have found a path!
|
||||
const midpoint = [...seenL].find(x => seenR.has(x))
|
||||
if (!midpoint) continue
|
||||
// console.log("Midpoint:", midpoint)
|
||||
|
||||
// Concatenate the paths from the left and right to get the shortest path
|
||||
// Flip the path from the right so it's in the correct order
|
||||
const path = backL.get(midpoint)!.copy()
|
||||
path.edges.push(...backR.get(midpoint)!.edges.reverse())
|
||||
return path
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import * as Colors from "https://deno.land/[email protected]/fmt/colors.ts"
|
||||
|
||||
type UUID = ReturnType<typeof crypto.randomUUID>
|
||||
|
||||
export type VertexType<G> = G extends Graph<infer T, infer V> ? Vertex<T, V> : never
|
||||
export type EdgeType<G> = G extends Graph<infer T, infer V> ? Edge<T, V> : never
|
||||
export type PathType<G> = G extends Graph<infer T, infer V> ? Path<T, V> : never
|
||||
/** Given an edge, this function should return the weight of that edge. */
|
||||
export type WeightFn<Graph> = (this: Graph, e: EdgeType<Graph>) => number
|
||||
|
||||
interface GraphOpts<G> {
|
||||
directed: boolean
|
||||
weights: WeightFn<G>
|
||||
}
|
||||
|
||||
interface EdgeOpts {
|
||||
directed: boolean
|
||||
id: UUID
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a graph, consisting of vertices and edges.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-26
|
||||
*/
|
||||
export class Graph<vData, eData> {
|
||||
/** A map of the vertices in the graph. To add a new vertex, please use the {@link Graph.createVertex} method instead. */
|
||||
readonly vertices: Map<UUID, Vertex<vData, eData>>
|
||||
/** A map of the edges in the graph. To add a new edge, please use the {@link Graph.createEdge} method instead. */
|
||||
readonly edges: Map<UUID, Edge<vData, eData>>
|
||||
/**
|
||||
* Whether or not edges created in the graph are directed when the value is omitted.
|
||||
* Note that edges created with the `directed` option will use that value instead.
|
||||
*/
|
||||
directed: boolean
|
||||
/** A function that returns the weight of an edge. */
|
||||
weights: WeightFn<typeof this>
|
||||
|
||||
constructor(opts: Partial<GraphOpts<Graph<vData, eData>>> = {}) {
|
||||
const options: GraphOpts<Graph<vData, eData>> = {
|
||||
directed: false,
|
||||
weights: () => 1,
|
||||
...opts
|
||||
}
|
||||
this.directed = options.directed
|
||||
this.weights = options.weights
|
||||
this.vertices = new Map()
|
||||
this.edges = new Map()
|
||||
}
|
||||
|
||||
createVertex(data: vData, id: UUID = crypto.randomUUID()): Vertex<vData, eData> {
|
||||
const V = new Vertex(this, id, data)
|
||||
this.vertices.set(id, V)
|
||||
return V
|
||||
}
|
||||
|
||||
deleteVertex(V: Vertex<vData, eData>) {
|
||||
// Delete all edges connected to V, then delete V
|
||||
for (const E of V.adjacentEdges) E.delete()
|
||||
this.vertices.delete(V.id)
|
||||
}
|
||||
|
||||
createEdge(u: Vertex<vData, eData>, v: Vertex<vData, eData>, data: eData, opts: Partial<EdgeOpts> = {}) {
|
||||
const options: EdgeOpts = {
|
||||
directed: this.directed,
|
||||
id: crypto.randomUUID(),
|
||||
...opts
|
||||
}
|
||||
const E = new Edge(this, options.id, u, v, options.directed, data)
|
||||
this.edges.set(options.id, E)
|
||||
v.adjacentEdges.add(E)
|
||||
u.adjacentEdges.add(E)
|
||||
return E
|
||||
}
|
||||
|
||||
deleteEdge(E: Edge<vData, eData>) {
|
||||
// Remove me from the caches of my connecting nodes, then delete me
|
||||
E.u.adjacentEdges.delete(E)
|
||||
E.v.adjacentEdges.delete(E)
|
||||
this.edges.delete(E.id)
|
||||
}
|
||||
|
||||
|
||||
createPath(start: Vertex<vData, eData>) {
|
||||
return new Path<vData, eData>(this, start)
|
||||
}
|
||||
|
||||
toJSON(
|
||||
vertexReplacer: (this: Graph<vData, eData>, data: vData) => unknown = data => data,
|
||||
edgeReplacer: (this: Graph<vData, eData>, data: eData) => unknown = data => data
|
||||
) {
|
||||
return {
|
||||
"@graph": Graph.signature,
|
||||
v: Array.from(this.vertices.values()).map(v => v.toJSON(vertexReplacer)),
|
||||
e: Array.from(this.edges.values()).map(e => e.toJSON(edgeReplacer))
|
||||
}
|
||||
}
|
||||
|
||||
static fromJSON<vData, eData>(
|
||||
json: unknown,
|
||||
vertexReviver: (this: Graph<vData, eData>, json: unknown) => vData = data => data as vData,
|
||||
edgeReviver: (this: Graph<vData, eData>, json: unknown) => eData = data => data as eData
|
||||
) {
|
||||
const G = new Graph<vData, eData>()
|
||||
|
||||
if (!json || typeof json !== 'object') throw Error("Failed to parse: Unexpected object!")
|
||||
const obj = json as Record<string, unknown>
|
||||
if (obj['@graph'] !== Graph.signature) throw Error("Failed to parse: The object isn't a graph!")
|
||||
|
||||
|
||||
if (!Array.isArray(obj.v)) throw Error("Failed to parse: The vertices property isn't an array!")
|
||||
if (!Array.isArray(obj.e)) throw Error("Failed to parse: The edges property isn't an array!")
|
||||
|
||||
for (const V of obj.v) {
|
||||
if (!Array.isArray(V)) throw Error("Failed to parse: One of the vertices isn't a tuple!")
|
||||
const [uuid, data] = V as [UUID, vData]
|
||||
if (typeof uuid != 'string') throw Error("Failed to parse: Unexpected UUID!")
|
||||
G.createVertex(vertexReviver.call(G, data), uuid)
|
||||
}
|
||||
|
||||
for (const E of obj.e) {
|
||||
if (!Array.isArray(E)) throw Error("Failed to parse: One of the edges isn't a tuple!")
|
||||
const [uuid, u, v, isDirected, data] = E as [UUID, UUID, UUID, boolean, eData]
|
||||
if (typeof uuid != 'string') throw Error("Failed to parse: UUID wasn't a string!")
|
||||
if (typeof u != 'string') throw Error("Failed to parse: U wasn't a string!")
|
||||
if (typeof v != 'string') throw Error("Failed to parse: V wasn't a string!")
|
||||
|
||||
const U = G.vertices.get(u)
|
||||
const V = G.vertices.get(v)
|
||||
|
||||
if (!U) throw Error(`Vertex ${U} doesn't exist!`)
|
||||
if (!V) throw Error(`Vertex ${V} doesn't exist!`)
|
||||
|
||||
G.createEdge(U, V, edgeReviver.call(G, data), {
|
||||
id: uuid,
|
||||
directed: isDirected
|
||||
})
|
||||
}
|
||||
|
||||
return G
|
||||
}
|
||||
|
||||
static signature = "Made with love by MindfulMinun <https://benjic.xyz>" as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a vertex in a graph.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-26
|
||||
*/
|
||||
|
||||
export class Vertex<VertexData, EdgeData> {
|
||||
/* The graph this vertex belongs to */
|
||||
graph: Graph<VertexData, EdgeData>
|
||||
/* The value stored in this vertex, up to you! */
|
||||
data: VertexData
|
||||
/* The unique identifier of this vertex */
|
||||
id: UUID
|
||||
/* The edges connected to this vertex */
|
||||
adjacentEdges: Set<Edge<VertexData, EdgeData>>
|
||||
|
||||
constructor(parentGraph: Graph<VertexData, EdgeData>, id: UUID, data: VertexData) {
|
||||
this.graph = parentGraph
|
||||
this.data = data
|
||||
this.id = id
|
||||
this.adjacentEdges = new Set()
|
||||
}
|
||||
|
||||
delete() { this.graph.deleteVertex(this) }
|
||||
|
||||
toJSON(replacer: (this: Graph<VertexData, EdgeData>, data: VertexData) => unknown = data => data) {
|
||||
return [this.id, replacer.call(this.graph, this.data)]
|
||||
}
|
||||
|
||||
toString() {
|
||||
return Colors.cyan(`<${this.id.slice(0, 8)}>: ${this.data}`)
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")]() {
|
||||
return this.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an edge in a graph which connects two vertices.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-07-26
|
||||
*/
|
||||
export class Edge<vData, eData> {
|
||||
/** The graph this edge belongs to */
|
||||
graph: Graph<vData, eData>
|
||||
/** The value stored in this edge, up to you! */
|
||||
data: eData
|
||||
/** A unique identifier for this edge. */
|
||||
id: UUID
|
||||
/** If directed, the vertex that this edge is coming from. */
|
||||
u: Vertex<vData, eData>
|
||||
/** If directed, the vertex that this edge is going to */
|
||||
v: Vertex<vData, eData>
|
||||
/** Whether or not this edge is directed */
|
||||
directed: boolean
|
||||
|
||||
constructor(
|
||||
parentGraph: Graph<vData, eData>,
|
||||
id: UUID,
|
||||
u: Vertex<vData, eData>,
|
||||
v: Vertex<vData, eData>,
|
||||
directed: boolean,
|
||||
data: eData
|
||||
) {
|
||||
this.graph = parentGraph
|
||||
this.data = data
|
||||
this.id = id
|
||||
this.directed = directed
|
||||
|
||||
if (u.graph !== v.graph || u.graph !== parentGraph)
|
||||
throw Error("Both vertices and the new edge must all belong to the same graph.")
|
||||
|
||||
this.u = u
|
||||
this.v = v
|
||||
}
|
||||
|
||||
/** Given one vertex, get the opposite vertex of this edge */
|
||||
not(v: typeof this.u | typeof this.v) {
|
||||
return this.v !== v ? this.v : this.u
|
||||
}
|
||||
|
||||
delete() { this.graph.deleteEdge(this) }
|
||||
|
||||
toJSON(replacer: (this: Graph<vData, eData>, data: eData) => unknown = data => data) {
|
||||
// tuple!
|
||||
return [
|
||||
this.id,
|
||||
this.u.id,
|
||||
this.v.id,
|
||||
this.directed,
|
||||
replacer.call(this.graph, this.data)
|
||||
]
|
||||
}
|
||||
|
||||
toString() {
|
||||
return Colors.magenta(`<${this.id.slice(0, 8)}>: ${this.u} ${!this.directed ? '<' : ''}--> ${this.v}`)
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")]() {
|
||||
return this.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export class Path<vData, eData> {
|
||||
/** The graph this path belongs to */
|
||||
graph: Graph<vData, eData>
|
||||
/** The vertex this path starts with */
|
||||
start: Vertex<vData, eData> | null
|
||||
/** The edges belonging to this path */
|
||||
edges: Edge<vData, eData>[]
|
||||
/** An internal cache of the vertices. */
|
||||
#vertices: Vertex<vData, eData>[] | null
|
||||
|
||||
constructor(graph: Graph<vData, eData>, start?: Vertex<vData, eData>) {
|
||||
this.graph = graph
|
||||
this.edges = []
|
||||
this.start = start ?? null
|
||||
this.#vertices = null
|
||||
}
|
||||
|
||||
addEdge(e: Edge<vData, eData>) {
|
||||
if (!this.start) this.start = e.u
|
||||
if (e.graph !== this.graph) throw Error("The edges in a path must all belong to the same graph.")
|
||||
this.edges.push(e)
|
||||
this.#vertices = null
|
||||
}
|
||||
|
||||
/** The vertices belonging to this path */
|
||||
get vertices() {
|
||||
if (this.#vertices) return this.#vertices
|
||||
|
||||
const vertices: Vertex<vData, eData>[] = []
|
||||
if (!this.start) return vertices
|
||||
|
||||
let current = this.start
|
||||
vertices.push(current)
|
||||
|
||||
for (const e of this.edges) {
|
||||
current = e.u !== current ? e.u : e.v
|
||||
vertices.push(current)
|
||||
}
|
||||
|
||||
this.#vertices = vertices
|
||||
return this.#vertices
|
||||
}
|
||||
|
||||
copy() {
|
||||
const path = new Path<vData, eData>(this.graph, this.start ?? undefined)
|
||||
path.edges = this.edges.slice()
|
||||
path.#vertices = this.#vertices?.slice() ?? null
|
||||
return path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { fill } from "../../core/iterable.ts"
|
||||
import * as Colors from "https://deno.land/[email protected]/fmt/colors.ts"
|
||||
|
||||
class Matrix {
|
||||
/** The number of rows in this matrix. */
|
||||
m: number
|
||||
/** The number of columns in this matrix. */
|
||||
n: number
|
||||
/** The values stored in this matrix, in row-major order. */
|
||||
entries: number[]
|
||||
|
||||
constructor(m: number, n: number, entries: number[]) {
|
||||
const expectedLength = m * n
|
||||
if (entries.length !== expectedLength) {
|
||||
throw new Error(`Expected ${expectedLength} entries, got ${entries.length}`)
|
||||
}
|
||||
this.m = m
|
||||
this.n = n
|
||||
this.entries = entries
|
||||
}
|
||||
|
||||
/** Performs naive matrix multiplication. */
|
||||
rightMultiply(right: Matrix) {
|
||||
if (this.n !== right.m) {
|
||||
throw new Error(`Cannot multiply ${this.m}x${this.n} matrix by ${right.m}x${right.n} matrix.`)
|
||||
}
|
||||
const result = Matrix.null(this.m, right.n)
|
||||
for (let i = 0; i < this.m; i++) {
|
||||
for (let j = 0; j < right.n; j++) {
|
||||
let sum = 0
|
||||
for (let k = 0; k < this.n; k++) {
|
||||
sum += this.get(i, k) * right.get(k, j)
|
||||
}
|
||||
result.set(i, j, sum)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
leftMultiply(left: Matrix) {
|
||||
return left.rightMultiply(this)
|
||||
}
|
||||
|
||||
set(row: number, col: number, value: number) {
|
||||
const offset = this.#calculateOffset(row, col)
|
||||
this.entries[offset] = value
|
||||
}
|
||||
|
||||
get(row: number, col: number) {
|
||||
const offset = this.#calculateOffset(row, col)
|
||||
return this.entries[offset]
|
||||
}
|
||||
|
||||
toString() {
|
||||
const strs = this.entries.map(e => e.toString())
|
||||
const maxLen = Math.max(...strs.map(s => s.length))
|
||||
const padded = strs.map(s => s.padStart(maxLen, ' '))
|
||||
|
||||
let out = ""
|
||||
for (let i = 0; i < this.m; i++) {
|
||||
if (i != 0) out += '\n'
|
||||
out += Colors.gray(i === 0 ? '┌ ' : i === this.m - 1 ? '└ ' : '│ ')
|
||||
// out += Colors.gray('│ ')
|
||||
|
||||
for (let j = 0; j < this.n; j++) {
|
||||
const offset = this.#calculateOffset(i, j)
|
||||
const value = this.entries[offset]
|
||||
const txt = padded[offset]
|
||||
out += (value == 0 ? Colors.gray(txt) : txt) + ' '
|
||||
}
|
||||
out += Colors.gray(i === 0 ? '┐ ' : i === this.m - 1 ? '┘ ' : '│ ')
|
||||
// out += Colors.gray('│')
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
[Symbol.for("Deno.customInspect")]() {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
#calculateOffset(row: number, col: number) {
|
||||
if (!(0 <= row || row < this.m)) {
|
||||
throw new Error(`Row ${row} is out of bounds for ${this.m}x${this.n} matrix.`)
|
||||
}
|
||||
if (!(0 <= col || col < this.n)) {
|
||||
throw new Error(`Column ${col} is out of bounds for ${this.m}x${this.n} matrix.`)
|
||||
}
|
||||
return row * this.n + col
|
||||
}
|
||||
|
||||
copy() {
|
||||
return new Matrix(this.m, this.n, this.entries.slice())
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs Gaussian elimination on this matrix, transforming it into an upper triangular matrix.
|
||||
* Note that this method does NOT mutate the original matrix, instead returning a new one.
|
||||
* If the matrix is singular, null is returned.
|
||||
* @author MindfulMinun
|
||||
* @since 2023-01-06
|
||||
*/
|
||||
toUpperTriangular() {
|
||||
const A = this.copy()
|
||||
const m = A.m
|
||||
const n = A.n
|
||||
for (let i = 0; i < m; i++) {
|
||||
// Find the pivot row.
|
||||
let pivotRow = i
|
||||
for (let j = i + 1; j < m; j++) {
|
||||
if (Math.abs(A.get(j, i)) > Math.abs(A.get(pivotRow, i))) {
|
||||
pivotRow = j
|
||||
}
|
||||
}
|
||||
|
||||
// Swap the pivot row with the current row.
|
||||
for (let j = 0; j < n; j++) {
|
||||
const temp = A.get(i, j)
|
||||
A.set(i, j, A.get(pivotRow, j))
|
||||
A.set(pivotRow, j, temp)
|
||||
}
|
||||
|
||||
// Eliminate the current column.
|
||||
for (let j = i + 1; j < m; j++) {
|
||||
const factor = A.get(j, i) / A.get(i, i)
|
||||
if (!Number.isFinite(factor)) return null
|
||||
for (let k = i; k < n; k++) {
|
||||
A.set(j, k, A.get(j, k) - factor * A.get(i, k))
|
||||
}
|
||||
}
|
||||
}
|
||||
return A
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new matrix with the given dimensions and entries.
|
||||
* All entries are initialized to 0.
|
||||
*/
|
||||
static null(m: number, n: number) {
|
||||
const zeroes = Array.from(fill(0, m * n))
|
||||
return new Matrix(m, n, zeroes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a square identity matrix with the given dimension.
|
||||
* An identity matrix is a square matrix with 1s on the diagonal and 0s everywhere else.
|
||||
*/
|
||||
static identity(n: number) {
|
||||
const I = Matrix.null(n, n)
|
||||
for (let i = 0; i < n; i++) {
|
||||
I.set(i, i, 1)
|
||||
}
|
||||
return I
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (import.meta.main) {
|
||||
// const A = new Matrix(3, 3, [
|
||||
// 2, -1, 0,
|
||||
// -1, 2, -1,
|
||||
// 0, -3, 4
|
||||
// ])
|
||||
const A = new Matrix(4, 4, [
|
||||
16, 2, 3, 13,
|
||||
5 , 11, 10, 8,
|
||||
9 , 7, 6, 12,
|
||||
4 , 14, 15, 1,
|
||||
])
|
||||
const x = new Matrix(3, 1, [0, 0, 1])
|
||||
// const A = Matrix.identity(10)
|
||||
|
||||
console.log(A)
|
||||
console.log(A.toUpperTriangular())
|
||||
// console.log(x)
|
||||
// const b = A.rightMultiply(x)
|
||||
// console.log(b)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* A cache of prime numbers.
|
||||
*
|
||||
* This cache is unfortunately bounded by the memory constraints JavaScript
|
||||
* imposes on arrays. This means the highest prime number that can be generated
|
||||
* is NOT anywhere close to `Number.MAX_SAFE_INTEGER` (2^53 - 1), but instead
|
||||
* much lower, at around the (2^32 - 2)th prime.
|
||||
* @private
|
||||
*/
|
||||
const knownPrimesArr: number[] = [2]
|
||||
|
||||
/**
|
||||
* Yields prime numbers in ascending order
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* This function has a heavy memory footprint, is unfortunately bounded
|
||||
* by the memory constraints JavaScript imposes on arrays. This means
|
||||
* the highest prime number that can be generated is NOT `Number.MAX_SAFE_INTEGER`, but
|
||||
* instead the (2^32 - 2)th prime.
|
||||
*
|
||||
* However, since primality only needs to be verified for numbers up to sqrt(n), this
|
||||
* function is sufficient for telling if any number between 2 and `Number.MAX_SAFE_INTEGER` is prime
|
||||
*
|
||||
* @example
|
||||
* for (const prime of primes()) {
|
||||
* if (prime > 100) break
|
||||
* console.log(prime)
|
||||
* }
|
||||
*
|
||||
* @author MindfulMinun
|
||||
* @since 2022-10-13
|
||||
*/
|
||||
export function* primes(): Generator<number, never, undefined> {
|
||||
// First we yield all the known primes
|
||||
// yield* knownPrimes.values()
|
||||
yield* knownPrimesArr
|
||||
|
||||
// Then we generate new primes using the sieve of Eratosthenes
|
||||
while (true) {
|
||||
const length = knownPrimesArr.length
|
||||
let n = knownPrimesArr[length - 1] + 1
|
||||
|
||||
while (!_isPrime(n)) n++
|
||||
|
||||
if (2 ** 32 - 3 < length) throw Error("Too many primes!")
|
||||
|
||||
knownPrimesArr.push(n)
|
||||
yield n
|
||||
}
|
||||
}
|
||||
|
||||
function _isPrime(n: number) {
|
||||
if (n % 1 !== 0) return false
|
||||
if (n < 2) return false
|
||||
|
||||
if (Number.MAX_SAFE_INTEGER < n) throw Error("Number is too large to verify X(")
|
||||
|
||||
const sqrt = Math.ceil(Math.sqrt(n))
|
||||
|
||||
for (const prime of knownPrimesArr) {
|
||||
if (n % prime === 0) return false
|
||||
if (sqrt < prime) break
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a given number is prime.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-10-13
|
||||
*/
|
||||
export function isPrime(n: number): boolean {
|
||||
if (n % 1 !== 0) return false
|
||||
if (n < 2) return false
|
||||
|
||||
// To verify if a number is prime, we only need to check if it is divisible by
|
||||
// any of the primes less than or equal to its square root.
|
||||
|
||||
const sqrt = Math.ceil(Math.sqrt(n))
|
||||
for (const prime of primes()) {
|
||||
if (n % prime === 0) return false
|
||||
if (sqrt < prime) break
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Data structure allowing for the insertion and removal of
|
||||
* elements in a LIFO manner in `O(1)` time.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-10-22
|
||||
*/
|
||||
export class Stack<T> implements Iterable<T> {
|
||||
#elements: Record<number, T>
|
||||
#head: number
|
||||
|
||||
constructor(initials?: Iterable<T>) {
|
||||
this.#elements = {}
|
||||
this.#head = 0
|
||||
if (!initials) return
|
||||
for (const el of initials) this.push(el)
|
||||
}
|
||||
|
||||
/** Add an element to the stack */
|
||||
push(element: T): void {
|
||||
this.#elements[this.#head++] = element
|
||||
}
|
||||
|
||||
/** Remove an element from the stack and return it. */
|
||||
pop(): T | undefined {
|
||||
if (this.length === 0) return undefined
|
||||
const el = this.#elements[--this.#head]
|
||||
delete this.#elements[this.#head]
|
||||
return el
|
||||
}
|
||||
|
||||
/** Preview the topmost element of the stack without removing it. */
|
||||
peek(): T | undefined {
|
||||
if (this.length === 0) return undefined
|
||||
return this.#elements[this.#head - 1]
|
||||
}
|
||||
|
||||
/** The number of elements remaining in the stack */
|
||||
get length() { return this.#head }
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
while (this.length !== 0) yield this.pop()!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure allowing for the insertion and removal of
|
||||
* elements in a FIFO manner in `O(1)` time.
|
||||
* @author MindfulMinun
|
||||
* @since 2022-10-22
|
||||
*/
|
||||
export class Queue<T> implements Iterable<T> {
|
||||
#elements: Record<number, T>
|
||||
#head: number
|
||||
#tail: number
|
||||
|
||||
constructor(initials?: Iterable<T>) {
|
||||
this.#elements = {}
|
||||
this.#head = 0
|
||||
this.#tail = 0
|
||||
if (!initials) return
|
||||
for (const el of initials) this.enqueue(el)
|
||||
}
|
||||
|
||||
/** Add an element to the queue */
|
||||
enqueue(element: T): void {
|
||||
this.#elements[this.#tail++] = element
|
||||
}
|
||||
|
||||
/** Remove an element from the queue and return it. */
|
||||
dequeue(): T | undefined {
|
||||
if (this.length === 0) return undefined
|
||||
const el = this.#elements[this.#head]
|
||||
delete this.#elements[this.#head++]
|
||||
return el
|
||||
}
|
||||
|
||||
/** Preview the next element in the queue without removing it. */
|
||||
peek(): T | undefined {
|
||||
if (this.length === 0) return undefined
|
||||
return this.#elements[this.#head]
|
||||
}
|
||||
|
||||
/** The number of elements remaining in the queue */
|
||||
get length() { return this.#tail - this.#head }
|
||||
|
||||
*[Symbol.iterator]() {
|
||||
while (this.length !== 0) yield this.dequeue()!
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user