@freeword/meta
Version:
Meta package for Freeword: exports all core types, constants, and utilities from the src/ directory.
344 lines (309 loc) • 17.7 kB
text/typescript
import _ /**/ from 'lodash'
// import * as UF from './UF.ts'
import type * as TY from '../types.ts'
import * as WordbitsTables from './WordbitsTables.ts'
//
const BITS_SMALLEST_26 = 0b11_1111_1111_1111_1111_1111_1111
const BITS_SMALLEST_8 = 0b1111_1111
const BITS_SMALLEST_2 = 0b11
const WordbitsMask = BITS_SMALLEST_26
// const BITS_SMALLEST_14 = 0b11_1111_1111_1111
// const BITS_SMALLEST_12 = 0b1111_1111_1111
// const BITS_SMALLEST_7 = 0b111_1111
/** Which letters in A are missing from B? | `A - B` | Wordbits | Difference | `A & ~B & WordbitMask` | */
export function aMinusB(aBits: TY.WordbitsT, bBits: TY.WordbitsT): TY.WordbitsT {
return (aBits & (~bBits)) & WordbitsMask
}
/** Which letters in A are missing from B? | `A - B` | Wordbits | Difference | `A & ~B & WordbitMask` | */
export function aMinusAll(aBits: TY.WordbitsT, ...bBitses: readonly TY.WordbitsT[]): TY.WordbitsT {
return (aBits & (~unions(...bBitses))) & WordbitsMask
}
/** Which letters in A are missing from B? | `A - B` | Wordbits | Difference | `A & ~B & WordbitMask` | */
export function subtract(aBits: TY.WordbitsT, bBits: TY.WordbitsT): TY.WordbitsT {
return (aBits & (~bBits)) & WordbitsMask
}
/** Which letters appear in either A or B (or both)? | `A ∪ B` | Wordbits | Union | `A \| B` | */
export function union(aBits: TY.WordbitsT, bBits: TY.WordbitsT): TY.WordbitsT {
return aBits | bBits
}
/** Which letters appear in any of the given sets? | `A ∪ B ∪ C` | Wordbits | Union | `A \| B \| C` | */
export function unions(...bitses: TY.WordbitsT[]): TY.WordbitsT {
return bitses.reduce((acc, bits) => acc | bits, 0)
}
/** Which letters appear in either A or B (or both)? | `A ∪ B` | Wordbits | Union | `A \| B` | */
export function inEither(aBits: TY.WordbitsT, bBits: TY.WordbitsT): TY.WordbitsT {
return aBits | bBits
}
/** Which letters do both A and B have in common? | `A ∩ B` | Wordbits | Intersection | `A & B` | */
export function intersection(aBits: TY.WordbitsT, bBits: TY.WordbitsT): TY.WordbitsT {
return aBits & bBits
}
/** Which letters appear in all of the given sets? | `A ∩ B ∩ C` | Wordbits | Intersection | `A & B & C` | */
export function intersections(...bitses: TY.WordbitsT[]): TY.WordbitsT {
return bitses.reduce((acc, bits) => acc & bits, WordbitsMask)
}
/** Which letters do both A and B have in common? | `A ∩ B` | Wordbits | Intersection | `A & B` | */
export function inBoth(aBits: TY.WordbitsT, bBits: TY.WordbitsT): TY.WordbitsT {
return aBits & bBits
}
/** Which letters are in A, or in B, but not in both? | `A ∆ B` | Wordbits | Symmetric Difference | `A ^ B` | */
export function inEitherNotBoth(aBits: TY.WordbitsT, bBits: TY.WordbitsT): TY.WordbitsT {
return aBits ^ bBits
}
/** Which letters are in A, or in B, but not in both? | `A ∆ B` | Wordbits | Symmetric Difference | `A ^ B` | */
export function xor(aBits: TY.WordbitsT, bBits: TY.WordbitsT): TY.WordbitsT {
return aBits ^ bBits
}
/** Do A and B share any letters at all? | `A ∩ B ≠ ∅` | boolean | Overlap | `(A & B) !== 0` | */
export function overlaps(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return (aBits & bBits) !== 0
}
/** Do A and B have no letters in common? | `A ∩ B = ∅` | boolean | Disjoint | `(A & B) === 0` | */
export function hasNoOverlap(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return (aBits & bBits) === 0
}
/** Do A and B have no letters in common? | `A ∩ B = ∅` | boolean | Disjoint | `(A & B) === 0` | */
export function disjoint(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return (aBits & bBits) === 0
}
/** Do A and B use exactly the same letters? | `A = B` | boolean | Equality | `A === B` | */
export function equals(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return aBits === bBits
}
export function missingLtrs(wordbits: TY.WordbitsT): TY.MissingBits {
return ~wordbits & BITS_SMALLEST_26
}
// /** @returns a mask suitable for use in s1MaskContainsS2 */
// export const containsMaskFor = missingLtrs
// /** @returns a mask suitable for use in s1MaskContainsS2 */
// export function containsMaskForWord(word: TY.Word | TY.Letter[]): TY.MissingBits {
// return missingLtrs(wordbitsForWord(word))
// }
// /** @returns true if s1 contains s2 -- NOTE: you must pass in `~s1`, not `s1` */
// export function containsMasked(maskA: TY.MissingBits, bitsB: TY.WordbitsT): boolean {
// // maskA has a bit set for each element maskA does not have,
// // so if maskA & bitsB has anything set, then bitsB has an element that maskA does not have
// return (maskA & bitsB) === 0
// }
/** Does A include *all* of the letters in B? | `A ⊆ B` | boolean | Subset | `(A & B) === A` | */
export function contains(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return (aBits & bBits) === bBits
}
/** Does A include *all* of the letters in B? | `A ⊆ B` | boolean | Subset | `(A & B) === A` | */
export function aHasAllOfB(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return (aBits & bBits) === bBits
}
/** Does A include all the letters in B *and more*? | `A ⊊ B` | boolean | Strict Subset | `(A & B) === A && A !== B` | */
export function aHasAllAndMoreB(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return ((aBits & bBits) === bBits) && aBits !== bBits
}
/** Does A include all the letters in B *and more*? | `A ⊊ B` | boolean | Strict Subset | `(A & B) === A && A !== B` | */
export function strictlyContains(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return ((aBits & bBits) === bBits) && aBits !== bBits
}
/** Is at least one letter in A missing from B? | `A ⊄ B` | boolean | Not Subset | `(A & B) !== A` | */
export function aHasMissingFromB(aBits: TY.WordbitsT, bBits: TY.WordbitsT): boolean {
return (aBits & bBits) !== aBits
}
/** Which letters are *not* in A? | `¬A` | Wordbits | Complement | `~A & WordbitMask` | */
export function missingFrom(aBits: TY.WordbitsT): TY.WordbitsT { return ~aBits & WordbitsMask }
/** @returns the wordbit mask for a single letter */
export function wordbitForLtr(ltr: TY.AtoZlo): number {
return WordbitsTables.ltrToWordbitsTable[ltr]
}
// how this works; using Little-endian for sanity, we have:
// | | | |
// wb * x15 = 000000000000001000000000000001000000000000001000000000000001
// 0nmlkjihgfedcba0nmlkjihgfedcba0nmlkjihgfedcba0nmlkjihgfedcba // multiplying by m15 puts a new copy every 15 bits
// r2 & m15 = 000100010001000100010001000100010001000100010001000100010001 // this is 1/15 in the same way 1/9 = 0.11111...
// l h d 0 k g c n j f b m i e a // mask picks out each letter once
// 12 8 4 15 11 7 3 14 10 6 2 13 9 5 1 // the modulo 15 sums each nybble (4-bits) -- don't understand why yet but it's to do with it being remainder after division by 15
// 1/15 = 0.1000100010001000100010001000100010001000100010001000100010001
// % 15 sums the nybbles
//
// Javascript does bitwise operations on 32-bit numbers,
// We can do it with a safe int if we only accept 7-bit words
// 00010001000100010001000100010001
/// gfedcbagfedcbagfedcbagfedcba
// 00010001000100010001000100010001
// d g c f b e a
// const MAGIC_NUMBER14x2 = 0x2000_4000_8001n
// const MAGIC_NUMBER14m2 = 0x0111_1111_1111_1111n
const MAGIC_NUMBER14r = 0b1111n
// 45| 30| 15| 0|
// | 15 | 15 | 15 |
const MAGIC_NUMBER14x = 0b0010_0000_0000_0000_0100_0000_0000_0000_1000_0000_0000_0001n
// 0x 2 0 0 0 4 0 0 0 8 0 0 1
// 0x 0____1____1____1____1____1____1____1____1____1____1____1____1____1____1____1
const MAGIC_NUMBER14m = 0b0001_0001_0001_0001_0001_0001_0001_0001_0001_0001_0001_0001_0001_0001_0001n
// |9 | 16 | 16 | 16 |
// 1. The multiply step repeats the bitfield four times, shifted by 2^15
// (the fourteen, and an extra one each time).
// That is, bit 0 will land at bits 3, 2, 1, 0 in their nybble.
// 2. The AND masks out all but the bottom bit in each nybble
// 3. The modulo 15 "sums" the nybbles
// We need to use a bigint to avoid overflow for over 8 bits
//
export function countBits14Magic(wordbits: bigint | number): number {
// mult by x, mask by m, read by r
return Number((((BigInt(wordbits) * MAGIC_NUMBER14x) & MAGIC_NUMBER14m) % MAGIC_NUMBER14r))
}
// gfedcbAgfedcbAgfedcbAgfedcbA
const MAGIC_NUMBER13xo = 0b000000001000000100000010000001
// d g c f b e a
const MAGIC_NUMBER13mo = 0b00010001000100010001000100010001
const MAGIC_NUMBER13ro = 0b1111
/** count bits in a 7-bit number */
export function countBits7Magic(wordbitso: number): number {
return (wordbitso * MAGIC_NUMBER13xo & MAGIC_NUMBER13mo) % MAGIC_NUMBER13ro
}
const MAGIC_NYBBLE_MASK = 0b00010001000100010001000100010001
// FEDCBAzyxwvutsrqponmlkjihgfedcba // mod 15 folds the nybbles into 4-bit numbers
// C___y___u___q___m___i___e___a
// D___z___v___r___n___j___f___b
// E___A___w___s___o___k___g___c
// F___B___x___t___p___l___h___d
/** count bits in a 32-bit number */
export function countBits32(wordbitso: number): number {
const bits1 = ( wordbitso & MAGIC_NYBBLE_MASK) // mod 15 folds the nybbles into 4-bit numbers
const bits2 = ((wordbitso >> 1) & MAGIC_NYBBLE_MASK)
const bits3 = ((wordbitso >> 2) & MAGIC_NYBBLE_MASK)
const bits4 = ((wordbitso >> 3) & MAGIC_NYBBLE_MASK)
// console.log(prettyBinaries([wordbitso, bits1, bits2, bits3, bits4]), (bits1 % 15), (bits2 % 15), (bits3 % 15), (bits4 % 15), (bits1 % 15) + (bits2 % 15) + (bits3 % 15) + (bits4% 15))
return ((bits1 % 15) + (bits2 % 15) + (bits3 % 15) + (bits4 % 15) )
}
/** @returns the number of bits set in the bitfield */
export function countUniqLtrs(wordbits: TY.WordbitsT): number {
return countBits32(wordbits)
}
export const countBits28 = countUniqLtrs
// == [Convert strings to/from wordbits]
/** @returns a string of only lowercase letters; given nil, returns '' */
export function normalizeWord(str: TY.StringMaybe): TY.Word {
return str?.toLowerCase().replace(/[^a-z]/g, '') ?? ''
}
/** @returns the wordbits encoding for a word:
* bit 0 (least significant / rightmost) is set if the word has an 'a',
* bit 1 is set if the word has a 'b', etc.
* ...
* bit 25 (most significant / leftmost) is set if the word has a 'z'
*/
export function wordbitsForWord(word: TY.Word | TY.Letter[]): TY.WordbitsT {
let bitfield = 0
for (const ltr of word) {
bitfield |= WordbitsTables.ltrToWordbitsTable[ltr as TY.AtoZlo]
}
return bitfield
}
/** @returns the wordbits encoding for a word. @see {wordbitsForWord} */
export function wordbitsForWordSafe(str: TY.StringMaybe): TY.WordbitsT {
return wordbitsForWord(normalizeWord(str))
}
export function missingWordbits(wordbits: TY.WordbitsT): TY.WordbitsT {
return ~wordbits & BITS_SMALLEST_26
}
/** ROT-13: trivially obscure/decode a string: a->n, b->o, ..., m->z, n->a, ..., z->m.
* Installation is the reverse of removal.
* @see {UF.rot13Word} */
export function rot13Wordbits(wordbits: TY.WordbitsT): TY.WordbitsT {
return ((wordbits << 13)| (wordbits>> 13)) & WordbitsMask
}
/** ROT-n: trivially obscure/decode a string: a->n, b->o, ..., m->z, n->a, ..., z->m.
* Installation is the reverse of removal.
* @see {rotNWord} */
export function rotNWordbits(wordbits: TY.WordbitsT, by: number): TY.WordbitsT {
// console.log(by, prettyBinaries([wordbits, (wordbits >> (26 - by)), ((wordbits << by) & WordbitsMask)], 28))
return ((wordbits >> (26 - by)) | ((wordbits << by) & WordbitsMask)) & WordbitsMask
}
/** @returns the unique letters in the word, in alphabetical order */
export function ltrsForWordbits(wordbits: TY.WordbitsT): TY.Shingle {
const bits00_06 = wordbits & 0b111_1111 as TY.AtoZnum
const bits07_13 = (wordbits >> 7) & 0b111_1111 as TY.AtoZnum
const bits14_20 = (wordbits >> 14) & 0b111_1111 as TY.AtoZnum
const bits21_25 = (wordbits >> 21) & 0b1_1111 as TY.AtoZnum
const uniqs = (
WordbitsTables.abcdefgFor00_06[bits00_06]! +
WordbitsTables.hijklmnFor07_13[bits07_13]! +
WordbitsTables.opqrstuFor14_20[bits14_20]! +
WordbitsTables.vwxyzFor21_25[bits21_25]!
)
return uniqs
}
/** @returns a fixed-width string of the wordbits, eg 0b00_0000_0100_1001_0001_1000_0100 for 'chimps' */
export function prettyWordbits(wordbits: TY.WordbitsT): string {
const bit00to07 = wordbits & BITS_SMALLEST_8
const bit08to15 = (wordbits >> 8) & BITS_SMALLEST_8
const bit16to23 = (wordbits >> 16) & BITS_SMALLEST_8
const bit24to27 = (wordbits >> 24) & BITS_SMALLEST_2
return '0b' + [
WordbitsTables.prettyBinaryTable2[bit24to27],
WordbitsTables.prettyBinaryTable8[bit16to23],
WordbitsTables.prettyBinaryTable8[bit08to15],
WordbitsTables.prettyBinaryTable8[bit00to07],
].join('_')
// return WordbitsTables.prettyBinaryTable26[wordbits]
}
export function prettyBinary53(val: number, num = 53): string {
return '0b' + (_.chunk(_.padStart(val.toString(2), 56, '0').slice(56 - num).split(''), 4).map((nibbles: string[]) => nibbles.join('')).join('_'))
}
export function prettyBinary32(val: number): string {
return prettyBinary53(val, 32)
}
export function prettyBinaries(vals: (number | bigint)[], num?: number | undefined): string {
return '\n' + vals.map((val) => prettyBinary53(Number(val), num)).join('\n')
}
export type DigestedWord = {
/** the original word */ word: TY.Word,
/** array of the word's letters, sorted */ ltrs: TY.AtoZlo[],
/** the first letter in the word */ beg: TY.AtoZlo,
/** the last letter in the word */ end: TY.AtoZlo,
/** wordbits of word's unique letters */ wordbits: TY.WordbitsT,
/** wordbits of letters absent from word */ missbits: TY.WordbitsT,
/** wordbit mask of the initial letter */ begbit: TY.WordbitsT,
/** wordbit mask of the final letter */ endbit: TY.WordbitsT,
/** uniq letters, sorted and joined */ uniqarr: TY.AtoZlo[],
/** duplicated letters, in alphabetic order; the first occurrence goes in uniqstr,
* all remaining occurrences go in dupearr. `uniqstr.length + dupearr.length === word.length`
* @example for 'minimises', uniqstr: 'eimns', dupestr: 'iims'
* @example for 'mines', uniqstr: 'eimns', dupestr: ''
* */ dupearr: TY.AtoZlo[],
}
/** @returns {DigestedWord} a summary of the wordbits encoding for a word
* @see {DigestedWord}
*/
export function digestWord(word: TY.Word): DigestedWord {
let wordbits = 0
let dupearr = [] as TY.AtoZlo[]
let uniqarr = [] as TY.AtoZlo[]
const ltrs = word.split('') as TY.AtoZlo[]
const beg = ltrs[0] as TY.AtoZlo
const end = ltrs[ltrs.length - 1] as TY.AtoZlo
ltrs.sort()
for (const ltr of ltrs) {
const ltrmask = WordbitsTables.ltrToWordbitsTable[ltr]
if (wordbits & ltrmask) { dupearr.push(ltr); continue }
uniqarr.push(ltr)
wordbits |= ltrmask
}
const missbits = (~wordbits & BITS_SMALLEST_26)
const begbit = WordbitsTables.ltrToWordbitsTable[beg]
const endbit = WordbitsTables.ltrToWordbitsTable[end]
// const headbits = wordbits & ~endbits & BITS_SMALLEST_12
// const tailbits = wordbits & ~begbits & BITS_SMALLEST_12
// const midbits = headbits & ~begbits & BITS_SMALLEST_12
return { word, ltrs, uniqarr, dupearr, beg, end, wordbits, missbits, begbit, endbit }
}
// export function wayDigestGameword(word: TY.Word, gamebitsTable: TY.GamebitsTable): WayDigestedWord {
// const gameltrs = _.keys(gamebitsTable) as readonly TY.Letter[]
// const digested = digestGameword(word, gamebitsTable)
// const { uniqarr, dupearr, gamebits, missbits, midbits, headbits, tailbits } = digested
// return {
// ...digested, // uniqstr: uniqarr.join(''),
// uniqstr: ltrsForGamebits(gamebits, gameltrs).join(''),
// dupestr: dupearr.join(''),
// missstr: ltrsForGamebits(missbits, gameltrs).join(''),
// headstr: ltrsForGamebits(headbits, gameltrs).join(''),
// midstr: ltrsForGamebits(midbits, gameltrs).join(''),
// tailstr: ltrsForGamebits(tailbits, gameltrs).join(''),
// }
// }