Merge pull request #3 from MindfulMinun/next

1.2.0
This commit is contained in:
mindfulminun authored and GitHub committed 2023-02-15 21:55:22 -06:00
commit e566edf635
18 files changed
+1487 -269

No files matched your search

+30
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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!]
}
+1 -1
View File
@@ -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
View File
@@ -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
}
}
View File
File renamed without changes.
+6 -19
View File
@@ -7,8 +7,8 @@ export const HTML_ESCAPES = {
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"/": "&#x2F;"
}
"/": "&#x2F;",
} 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])
}
+13
View File
@@ -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
-->
-94
View File
@@ -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
}
}
+70
View File
@@ -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
View File
@@ -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[] = []
+130
View File
@@ -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
}
+151
View File
@@ -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
}
}
+299
View File
@@ -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
}
}
+177
View File
@@ -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)
}
+86
View File
@@ -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
}
+89
View File
@@ -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()!
}
}