puzlink 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +35 -0
- package/dist/data/answerLengths.d.ts +10 -0
- package/dist/data/answerLengths.d.ts.map +1 -0
- package/dist/data/answerLengths.js +63 -0
- package/dist/data/answerLengths.js.map +1 -0
- package/dist/data/categories/compass.d.ts +3 -0
- package/dist/data/categories/compass.d.ts.map +1 -0
- package/dist/data/categories/compass.js +11 -0
- package/dist/data/categories/compass.js.map +1 -0
- package/dist/data/categories/countryAlpha2.d.ts +3 -0
- package/dist/data/categories/countryAlpha2.d.ts.map +1 -0
- package/dist/data/categories/countryAlpha2.js +252 -0
- package/dist/data/categories/countryAlpha2.js.map +1 -0
- package/dist/data/categories/countryAlpha3.d.ts +3 -0
- package/dist/data/categories/countryAlpha3.d.ts.map +1 -0
- package/dist/data/categories/countryAlpha3.js +252 -0
- package/dist/data/categories/countryAlpha3.js.map +1 -0
- package/dist/data/categories/daysOfTheWeek.d.ts +3 -0
- package/dist/data/categories/daysOfTheWeek.d.ts.map +1 -0
- package/dist/data/categories/daysOfTheWeek.js +10 -0
- package/dist/data/categories/daysOfTheWeek.js.map +1 -0
- package/dist/data/categories/elementSymbols.d.ts +3 -0
- package/dist/data/categories/elementSymbols.d.ts.map +1 -0
- package/dist/data/categories/elementSymbols.js +121 -0
- package/dist/data/categories/elementSymbols.js.map +1 -0
- package/dist/data/categories/greekLetters.d.ts +3 -0
- package/dist/data/categories/greekLetters.d.ts.map +1 -0
- package/dist/data/categories/greekLetters.js +27 -0
- package/dist/data/categories/greekLetters.js.map +1 -0
- package/dist/data/categories/months.d.ts +3 -0
- package/dist/data/categories/months.d.ts.map +1 -0
- package/dist/data/categories/months.js +15 -0
- package/dist/data/categories/months.js.map +1 -0
- package/dist/data/categories/natoAlphabet.d.ts +3 -0
- package/dist/data/categories/natoAlphabet.d.ts.map +1 -0
- package/dist/data/categories/natoAlphabet.js +29 -0
- package/dist/data/categories/natoAlphabet.js.map +1 -0
- package/dist/data/categories/numbers.d.ts +3 -0
- package/dist/data/categories/numbers.d.ts.map +1 -0
- package/dist/data/categories/numbers.js +16 -0
- package/dist/data/categories/numbers.js.map +1 -0
- package/dist/data/categories/romanNumerals.d.ts +3 -0
- package/dist/data/categories/romanNumerals.d.ts.map +1 -0
- package/dist/data/categories/romanNumerals.js +134 -0
- package/dist/data/categories/romanNumerals.js.map +1 -0
- package/dist/data/categories/solfege.d.ts +3 -0
- package/dist/data/categories/solfege.d.ts.map +1 -0
- package/dist/data/categories/solfege.js +11 -0
- package/dist/data/categories/solfege.js.map +1 -0
- package/dist/data/categories/usStateAbbreviations.d.ts +3 -0
- package/dist/data/categories/usStateAbbreviations.d.ts.map +1 -0
- package/dist/data/categories/usStateAbbreviations.js +53 -0
- package/dist/data/categories/usStateAbbreviations.js.map +1 -0
- package/dist/data/categories.d.ts +10 -0
- package/dist/data/categories.d.ts.map +1 -0
- package/dist/data/categories.js +31 -0
- package/dist/data/categories.js.map +1 -0
- package/dist/data/knownLogProbs.d.ts +6 -0
- package/dist/data/knownLogProbs.d.ts.map +1 -0
- package/dist/data/knownLogProbs.js +2975 -0
- package/dist/data/knownLogProbs.js.map +1 -0
- package/dist/data/morse.d.ts +2 -0
- package/dist/data/morse.d.ts.map +1 -0
- package/dist/data/morse.js +29 -0
- package/dist/data/morse.js.map +1 -0
- package/dist/data/scrabble.d.ts +2 -0
- package/dist/data/scrabble.d.ts.map +1 -0
- package/dist/data/scrabble.js +29 -0
- package/dist/data/scrabble.js.map +1 -0
- package/dist/features/index.d.ts +32 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +79 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/letterCount.d.ts +7 -0
- package/dist/features/letterCount.d.ts.map +1 -0
- package/dist/features/letterCount.js +121 -0
- package/dist/features/letterCount.js.map +1 -0
- package/dist/features/letterSequence.d.ts +7 -0
- package/dist/features/letterSequence.d.ts.map +1 -0
- package/dist/features/letterSequence.js +155 -0
- package/dist/features/letterSequence.js.map +1 -0
- package/dist/features/logProbCache.d.ts +16 -0
- package/dist/features/logProbCache.d.ts.map +1 -0
- package/dist/features/logProbCache.js +36 -0
- package/dist/features/logProbCache.js.map +1 -0
- package/dist/features/other.d.ts +4 -0
- package/dist/features/other.d.ts.map +1 -0
- package/dist/features/other.js +190 -0
- package/dist/features/other.js.map +1 -0
- package/dist/features/substring.d.ts +3 -0
- package/dist/features/substring.d.ts.map +1 -0
- package/dist/features/substring.js +146 -0
- package/dist/features/substring.js.map +1 -0
- package/dist/features/wordplay.d.ts +7 -0
- package/dist/features/wordplay.d.ts.map +1 -0
- package/dist/features/wordplay.js +387 -0
- package/dist/features/wordplay.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/affixDistribution.d.ts +26 -0
- package/dist/lib/affixDistribution.d.ts.map +1 -0
- package/dist/lib/affixDistribution.js +105 -0
- package/dist/lib/affixDistribution.js.map +1 -0
- package/dist/lib/counter.d.ts +23 -0
- package/dist/lib/counter.d.ts.map +1 -0
- package/dist/lib/counter.js +55 -0
- package/dist/lib/counter.js.map +1 -0
- package/dist/lib/distribution.d.ts +40 -0
- package/dist/lib/distribution.d.ts.map +1 -0
- package/dist/lib/distribution.js +176 -0
- package/dist/lib/distribution.js.map +1 -0
- package/dist/lib/lengthDistribution.d.ts +30 -0
- package/dist/lib/lengthDistribution.d.ts.map +1 -0
- package/dist/lib/lengthDistribution.js +137 -0
- package/dist/lib/lengthDistribution.js.map +1 -0
- package/dist/lib/letterBitset.d.ts +49 -0
- package/dist/lib/letterBitset.d.ts.map +1 -0
- package/dist/lib/letterBitset.js +101 -0
- package/dist/lib/letterBitset.js.map +1 -0
- package/dist/lib/letterDistribution.d.ts +60 -0
- package/dist/lib/letterDistribution.d.ts.map +1 -0
- package/dist/lib/letterDistribution.js +230 -0
- package/dist/lib/letterDistribution.js.map +1 -0
- package/dist/lib/letterIndices.d.ts +13 -0
- package/dist/lib/letterIndices.d.ts.map +1 -0
- package/dist/lib/letterIndices.js +41 -0
- package/dist/lib/letterIndices.js.map +1 -0
- package/dist/lib/logCounter.d.ts +23 -0
- package/dist/lib/logCounter.d.ts.map +1 -0
- package/dist/lib/logCounter.js +49 -0
- package/dist/lib/logCounter.js.map +1 -0
- package/dist/lib/logNum.d.ts +36 -0
- package/dist/lib/logNum.d.ts.map +1 -0
- package/dist/lib/logNum.js +193 -0
- package/dist/lib/logNum.js.map +1 -0
- package/dist/lib/memoize.d.ts +5 -0
- package/dist/lib/memoize.d.ts.map +1 -0
- package/dist/lib/memoize.js +104 -0
- package/dist/lib/memoize.js.map +1 -0
- package/dist/lib/util.d.ts +30 -0
- package/dist/lib/util.d.ts.map +1 -0
- package/dist/lib/util.js +111 -0
- package/dist/lib/util.js.map +1 -0
- package/dist/lib/wordlist.d.ts +66 -0
- package/dist/lib/wordlist.d.ts.map +1 -0
- package/dist/lib/wordlist.js +166 -0
- package/dist/lib/wordlist.js.map +1 -0
- package/dist/linkers/index.d.ts +34 -0
- package/dist/linkers/index.d.ts.map +1 -0
- package/dist/linkers/index.js +25 -0
- package/dist/linkers/index.js.map +1 -0
- package/dist/linkers/indexing.d.ts +5 -0
- package/dist/linkers/indexing.d.ts.map +1 -0
- package/dist/linkers/indexing.js +152 -0
- package/dist/linkers/indexing.js.map +1 -0
- package/dist/linkers/length.d.ts +5 -0
- package/dist/linkers/length.d.ts.map +1 -0
- package/dist/linkers/length.js +101 -0
- package/dist/linkers/length.js.map +1 -0
- package/dist/linkers/letterDistribution.d.ts +4 -0
- package/dist/linkers/letterDistribution.d.ts.map +1 -0
- package/dist/linkers/letterDistribution.js +46 -0
- package/dist/linkers/letterDistribution.js.map +1 -0
- package/dist/linkers/other.d.ts +5 -0
- package/dist/linkers/other.d.ts.map +1 -0
- package/dist/linkers/other.js +90 -0
- package/dist/linkers/other.js.map +1 -0
- package/dist/parse.d.ts +8 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +23 -0
- package/dist/parse.js.map +1 -0
- package/dist/puzlink.d.ts +84 -0
- package/dist/puzlink.d.ts.map +1 -0
- package/dist/puzlink.js +59 -0
- package/dist/puzlink.js.map +1 -0
- package/package.json +57 -0
- package/src/data/answerLengths.ts +63 -0
- package/src/data/categories/README.md +3 -0
- package/src/data/categories/compass.ts +1 -0
- package/src/data/categories/countryAlpha2.ts +251 -0
- package/src/data/categories/countryAlpha3.ts +251 -0
- package/src/data/categories/daysOfTheWeek.ts +1 -0
- package/src/data/categories/elementSymbols.ts +120 -0
- package/src/data/categories/greekLetters.ts +26 -0
- package/src/data/categories/months.ts +14 -0
- package/src/data/categories/natoAlphabet.ts +28 -0
- package/src/data/categories/numbers.ts +15 -0
- package/src/data/categories/romanNumerals.ts +133 -0
- package/src/data/categories/solfege.ts +1 -0
- package/src/data/categories/txt/compass.txt +8 -0
- package/src/data/categories/txt/daysOfTheWeek.txt +7 -0
- package/src/data/categories/txt/elementSymbols.txt +118 -0
- package/src/data/categories/txt/greekLetters.txt +24 -0
- package/src/data/categories/txt/months.txt +12 -0
- package/src/data/categories/txt/natoAlphabet.txt +26 -0
- package/src/data/categories/txt/numbers.txt +13 -0
- package/src/data/categories/txt/solfege.txt +8 -0
- package/src/data/categories/txt/usStateAbbreviations.txt +50 -0
- package/src/data/categories/usStateAbbreviations.ts +52 -0
- package/src/data/categories.ts +42 -0
- package/src/data/knownLogProbs.ts +2992 -0
- package/src/data/morse.ts +28 -0
- package/src/data/scrabble.ts +28 -0
- package/src/features/index.ts +120 -0
- package/src/features/letterCount.ts +174 -0
- package/src/features/letterSequence.ts +222 -0
- package/src/features/logProbCache.ts +48 -0
- package/src/features/other.ts +214 -0
- package/src/features/substring.ts +173 -0
- package/src/features/wordplay.ts +428 -0
- package/src/index.ts +3 -0
- package/src/lib/affixDistribution.ts +70 -0
- package/src/lib/counter.ts +71 -0
- package/src/lib/distribution.ts +162 -0
- package/src/lib/lengthDistribution.ts +108 -0
- package/src/lib/letterBitset.ts +123 -0
- package/src/lib/letterDistribution.ts +236 -0
- package/src/lib/letterIndices.ts +51 -0
- package/src/lib/logCounter.ts +74 -0
- package/src/lib/logNum.ts +193 -0
- package/src/lib/memoize.ts +136 -0
- package/src/lib/testUtils.ts +1 -0
- package/src/lib/util.ts +150 -0
- package/src/lib/wordlist.ts +162 -0
- package/src/linkers/index.ts +56 -0
- package/src/linkers/indexing.ts +194 -0
- package/src/linkers/length.ts +122 -0
- package/src/linkers/other.ts +117 -0
- package/src/parse.ts +20 -0
- package/src/puzlink.ts +141 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { LETTERS } from "../lib/letterDistribution.js";
|
|
2
|
+
import {
|
|
3
|
+
caesar,
|
|
4
|
+
enumerate,
|
|
5
|
+
interval,
|
|
6
|
+
mapProduct,
|
|
7
|
+
windows,
|
|
8
|
+
} from "../lib/util.js";
|
|
9
|
+
import type { Feature } from "./index.js";
|
|
10
|
+
|
|
11
|
+
function prependWith(letter: string): Feature {
|
|
12
|
+
return {
|
|
13
|
+
name: `can prepend ${letter}`,
|
|
14
|
+
property: (slug, { wordlist }) => {
|
|
15
|
+
const prepended = `${letter}${slug}`;
|
|
16
|
+
return wordlist.isWord(prepended)
|
|
17
|
+
? `${letter} + ${slug} = ${prepended}`
|
|
18
|
+
: null;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function prependAny(): Feature {
|
|
24
|
+
return {
|
|
25
|
+
name: "can prepend 1",
|
|
26
|
+
property: (slug, { wordlist }) => {
|
|
27
|
+
const prepended = wordlist.filterWords(
|
|
28
|
+
Array.from(LETTERS).map((letter) => `${letter}${slug}`),
|
|
29
|
+
);
|
|
30
|
+
return prepended.length === 0
|
|
31
|
+
? null
|
|
32
|
+
: `1 + ${slug} = ${prepended.join(", ")}`;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function appendWith(letter: string): Feature {
|
|
38
|
+
return {
|
|
39
|
+
name: `can append ${letter}`,
|
|
40
|
+
property: (slug, { wordlist }) => {
|
|
41
|
+
const appended = `${slug}${letter}`;
|
|
42
|
+
return wordlist.isWord(appended)
|
|
43
|
+
? `${slug} + ${letter} = ${appended}`
|
|
44
|
+
: null;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendAny(): Feature {
|
|
50
|
+
return {
|
|
51
|
+
name: "can append 1",
|
|
52
|
+
property: (slug, { wordlist }) => {
|
|
53
|
+
const appended = wordlist.filterWords(
|
|
54
|
+
Array.from(LETTERS).map((letter) => `${slug}${letter}`),
|
|
55
|
+
);
|
|
56
|
+
return appended.length === 0
|
|
57
|
+
? null
|
|
58
|
+
: `${slug} + 1 = ${appended.join(", ")}`;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function insertWith(letter: string): Feature {
|
|
64
|
+
return {
|
|
65
|
+
name: `can insert ${letter}`,
|
|
66
|
+
property: (slug, { wordlist }) => {
|
|
67
|
+
const allInserted = [];
|
|
68
|
+
for (let i = 0; i <= slug.length; i++) {
|
|
69
|
+
allInserted.push(`${slug.slice(0, i)}${letter}${slug.slice(i)}`);
|
|
70
|
+
}
|
|
71
|
+
const inserted = wordlist.filterWords(allInserted);
|
|
72
|
+
return inserted.length === 0
|
|
73
|
+
? null
|
|
74
|
+
: `${slug} insert ${letter} = ${inserted.join(", ")}`;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function insertAny(): Feature {
|
|
80
|
+
return {
|
|
81
|
+
name: "can insert 1",
|
|
82
|
+
property: (slug, { wordlist }) => {
|
|
83
|
+
const allInserted = [];
|
|
84
|
+
for (let i = 0; i <= slug.length; i++) {
|
|
85
|
+
for (const letter of LETTERS) {
|
|
86
|
+
allInserted.push(`${slug.slice(0, i)}${letter}${slug.slice(i)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const inserted = wordlist.filterWords(allInserted);
|
|
90
|
+
return inserted.length === 0
|
|
91
|
+
? null
|
|
92
|
+
: `${slug} insert 1 = ${inserted.join(", ")}`;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function behead(): Feature {
|
|
98
|
+
return {
|
|
99
|
+
name: "can behead 1",
|
|
100
|
+
property: (slug, { wordlist }) => {
|
|
101
|
+
const beheaded = slug.slice(1);
|
|
102
|
+
return wordlist.isWord(beheaded)
|
|
103
|
+
? `${slug} behead 1 = ${beheaded}`
|
|
104
|
+
: null;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function curtail(): Feature {
|
|
110
|
+
return {
|
|
111
|
+
name: "can curtail 1",
|
|
112
|
+
property: (slug, { wordlist }) => {
|
|
113
|
+
const curtailed = slug.slice(0, -1);
|
|
114
|
+
return wordlist.isWord(curtailed)
|
|
115
|
+
? `${slug} curtail 1 = ${curtailed}`
|
|
116
|
+
: null;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function deleteWith(letter: string): Feature {
|
|
122
|
+
return {
|
|
123
|
+
name: `can delete ${letter}`,
|
|
124
|
+
property: (slug, { wordlist }) => {
|
|
125
|
+
const allDeleted = [];
|
|
126
|
+
for (const [i, c] of enumerate(slug)) {
|
|
127
|
+
if (c === letter) {
|
|
128
|
+
allDeleted.push(`${slug.slice(0, i)}${slug.slice(i + 1)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const deleted = wordlist.filterWords(allDeleted);
|
|
132
|
+
return deleted.length === 0
|
|
133
|
+
? null
|
|
134
|
+
: `${slug} delete ${letter} = ${deleted.join(", ")}`;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function deleteAny(): Feature {
|
|
140
|
+
return {
|
|
141
|
+
name: "can delete 1",
|
|
142
|
+
property: (slug, { wordlist }) => {
|
|
143
|
+
const allDeleted = [];
|
|
144
|
+
for (const [i] of enumerate(slug)) {
|
|
145
|
+
allDeleted.push(`${slug.slice(0, i)}${slug.slice(i + 1)}`);
|
|
146
|
+
}
|
|
147
|
+
const deleted = wordlist.filterWords(allDeleted);
|
|
148
|
+
return deleted.length === 0
|
|
149
|
+
? null
|
|
150
|
+
: `${slug} delete 1 = ${deleted.join(", ")}`;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function takeOddOrEven(): Feature {
|
|
156
|
+
return {
|
|
157
|
+
name: "can take odd or even letters",
|
|
158
|
+
property: (slug, { wordlist }) => {
|
|
159
|
+
const odd = Array.from(slug)
|
|
160
|
+
.filter((_, i) => i % 2 === 1)
|
|
161
|
+
.join("");
|
|
162
|
+
const even = Array.from(slug)
|
|
163
|
+
.filter((_, i) => i % 2 === 0)
|
|
164
|
+
.join("");
|
|
165
|
+
const isOdd = wordlist.isWord(odd);
|
|
166
|
+
const isEven = wordlist.isWord(even);
|
|
167
|
+
if (!isOdd && !isEven) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (isOdd && isEven) {
|
|
171
|
+
return `${slug} take odd = ${odd}; take even = ${even}`;
|
|
172
|
+
}
|
|
173
|
+
return isOdd
|
|
174
|
+
? `${slug} take odd = ${odd}`
|
|
175
|
+
: `${slug} take even = ${even}`;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function changeTo(letter: string): Feature {
|
|
181
|
+
return {
|
|
182
|
+
name: `can change to ${letter}`,
|
|
183
|
+
property: (slug, { wordlist }) => {
|
|
184
|
+
const allChanged = [];
|
|
185
|
+
for (const [i, c] of enumerate(slug)) {
|
|
186
|
+
if (c !== letter) {
|
|
187
|
+
allChanged.push(`${slug.slice(0, i)}${letter}${slug.slice(i + 1)}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const changed = wordlist.filterWords(allChanged);
|
|
191
|
+
return changed.length === 0
|
|
192
|
+
? null
|
|
193
|
+
: `${slug} change to ${letter} = ${changed.join(", ")}`;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function changeAny(): Feature {
|
|
199
|
+
return {
|
|
200
|
+
name: "can change 1",
|
|
201
|
+
property: (slug, { wordlist }) => {
|
|
202
|
+
const allChanged = [];
|
|
203
|
+
for (const [i, c] of enumerate(slug)) {
|
|
204
|
+
for (const letter of LETTERS) {
|
|
205
|
+
if (c !== letter) {
|
|
206
|
+
allChanged.push(`${slug.slice(0, i)}${letter}${slug.slice(i + 1)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const changed = wordlist.filterWords(allChanged);
|
|
211
|
+
return changed.length === 0
|
|
212
|
+
? null
|
|
213
|
+
: `${slug} change 1 = ${changed.join(", ")}`;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function reverse(): Feature {
|
|
219
|
+
return {
|
|
220
|
+
name: "can reverse",
|
|
221
|
+
property: (slug, { wordlist }) => {
|
|
222
|
+
const reversed = slug.split("").reverse().join("");
|
|
223
|
+
return wordlist.isWord(reversed)
|
|
224
|
+
? `${slug} reversed = ${reversed}`
|
|
225
|
+
: null;
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function rotate(): Feature {
|
|
231
|
+
return {
|
|
232
|
+
name: "can rotate",
|
|
233
|
+
property: (slug, { wordlist }) => {
|
|
234
|
+
const candidates: [candidate: string, n: number][] = [];
|
|
235
|
+
for (let n = 1; n < slug.length; n++) {
|
|
236
|
+
candidates.push([slug.slice(n) + slug.slice(0, n), n]);
|
|
237
|
+
}
|
|
238
|
+
const rotates = wordlist.filterWordsUnder(candidates, (t) => t[0]);
|
|
239
|
+
if (rotates.length === 0) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const [first, ...rest] = rotates;
|
|
243
|
+
const [rotated, n] = first!;
|
|
244
|
+
const nStr =
|
|
245
|
+
n > slug.length / 2 ? (n - slug.length).toString() : n.toString();
|
|
246
|
+
return `${slug} rotate ${nStr} = ${rotated}${
|
|
247
|
+
rest.length > 0 ? ` (alt: ${rest.map((t) => t[0]).join(", ")})` : ""
|
|
248
|
+
}`;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function swapAdjacent(): Feature {
|
|
254
|
+
return {
|
|
255
|
+
name: "can swap adjacent letters",
|
|
256
|
+
property: (slug, { wordlist }) => {
|
|
257
|
+
const candidates: [candidate: string, i: number][] = [];
|
|
258
|
+
for (const [i, [a, b]] of enumerate(windows(slug, 2))) {
|
|
259
|
+
if (a !== b) {
|
|
260
|
+
candidates.push([
|
|
261
|
+
`${slug.slice(0, i)}${b}${a}${slug.slice(i + 2)}`,
|
|
262
|
+
i,
|
|
263
|
+
]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const swapped = wordlist.filterWordsUnder(candidates, ([w]) => w);
|
|
267
|
+
if (swapped.length === 0) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
const [first, ...rest] = swapped;
|
|
271
|
+
const swapIndices = `${(first![1] + 1).toString()}, ${(first![1] + 2).toString()}`;
|
|
272
|
+
return `${slug} swap ${swapIndices} = ${first![0]}${
|
|
273
|
+
rest.length > 0 ? ` (alt: ${rest.map((t) => t[0]).join(", ")})` : ""
|
|
274
|
+
}`;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function swapEnds(): Feature {
|
|
280
|
+
return {
|
|
281
|
+
name: "can swap ends",
|
|
282
|
+
property: (slug, { wordlist }) => {
|
|
283
|
+
if (slug.length < 2) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const candidate = `${slug.at(-1)!}${slug.slice(1, -1)}${slug.at(0)!}`;
|
|
287
|
+
return wordlist.isWord(candidate)
|
|
288
|
+
? `${slug} swap ends = ${candidate}`
|
|
289
|
+
: null;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function anagram(): Feature {
|
|
295
|
+
return {
|
|
296
|
+
name: "is anagram",
|
|
297
|
+
property: (slug, { wordlist }) => {
|
|
298
|
+
const anagrams = wordlist.anagrams(slug);
|
|
299
|
+
return anagrams.length === 0
|
|
300
|
+
? null
|
|
301
|
+
: `${slug} anagrammed = ${anagrams.join(", ")}`;
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function transaddWith(letter: string): Feature {
|
|
307
|
+
return {
|
|
308
|
+
name: `has transadd ${letter}`,
|
|
309
|
+
property: (slug, { wordlist }) => {
|
|
310
|
+
const transadds = wordlist.anagrams(`${slug}${letter}`, {
|
|
311
|
+
loose: true,
|
|
312
|
+
});
|
|
313
|
+
return transadds.length === 0
|
|
314
|
+
? null
|
|
315
|
+
: `${slug} transadd ${letter} = ${transadds.join(", ")}`;
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function transaddAny(): Feature {
|
|
321
|
+
return {
|
|
322
|
+
name: "has transadd 1",
|
|
323
|
+
property: (slug, { wordlist }) => {
|
|
324
|
+
const allTransadds = [];
|
|
325
|
+
for (const letter of LETTERS) {
|
|
326
|
+
for (const transadd of wordlist.anagrams(`${slug}${letter}`, {
|
|
327
|
+
loose: true,
|
|
328
|
+
})) {
|
|
329
|
+
allTransadds.push(transadd);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const transadds = wordlist.filterWords(allTransadds);
|
|
333
|
+
return transadds.length === 0
|
|
334
|
+
? null
|
|
335
|
+
: `${slug} transadd 1 = ${transadds.join(", ")}`;
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function transdeleteWith(letter: string): Feature {
|
|
341
|
+
return {
|
|
342
|
+
name: `has transdelete ${letter}`,
|
|
343
|
+
property: (slug, { wordlist }) => {
|
|
344
|
+
if (!slug.includes(letter)) {
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const transdeletes = wordlist.anagrams(slug.replace(letter, ""), {
|
|
348
|
+
loose: true,
|
|
349
|
+
});
|
|
350
|
+
return transdeletes.length === 0
|
|
351
|
+
? null
|
|
352
|
+
: `${slug} transdelete ${letter} = ${transdeletes.join(", ")}`;
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function transdeleteAny(): Feature {
|
|
358
|
+
return {
|
|
359
|
+
name: "has transdelete 1",
|
|
360
|
+
property: (slug, { wordlist }) => {
|
|
361
|
+
const allTransdeletes = [];
|
|
362
|
+
for (const letter of new Set(slug)) {
|
|
363
|
+
for (const transdelete of wordlist.anagrams(slug.replace(letter, ""), {
|
|
364
|
+
loose: true,
|
|
365
|
+
})) {
|
|
366
|
+
allTransdeletes.push(transdelete);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const transdeletes = wordlist.filterWords(allTransdeletes);
|
|
370
|
+
return transdeletes.length === 0
|
|
371
|
+
? null
|
|
372
|
+
: `${slug} transdelete 1 = ${transdeletes.join(", ")}`;
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function caesarShift(): Feature {
|
|
378
|
+
return {
|
|
379
|
+
name: "has caesar shift",
|
|
380
|
+
property: (slug, { wordlist }) => {
|
|
381
|
+
const candidates: [candidate: string, n: number][] = [
|
|
382
|
+
...interval(-12, -1),
|
|
383
|
+
...interval(1, 13),
|
|
384
|
+
].map((n) => [caesar(slug, n), n]);
|
|
385
|
+
const shifted = wordlist.filterWordsUnder(candidates, (t) => t[0]);
|
|
386
|
+
if (shifted.length === 0) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
const [first, ...rest] = shifted;
|
|
390
|
+
const [shiftedSlug, n] = first!;
|
|
391
|
+
return `${slug} caesar shift ${n.toString()} = ${shiftedSlug}${
|
|
392
|
+
rest.length > 0 ? ` (alt: ${rest.map((t) => t[0]).join(", ")})` : ""
|
|
393
|
+
}`;
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Features for wordplay: slugs that form a word when applying some sort of
|
|
400
|
+
* transformation.
|
|
401
|
+
*/
|
|
402
|
+
export function wordplayFeatures(): Feature[] {
|
|
403
|
+
return [
|
|
404
|
+
...mapProduct(prependWith, LETTERS),
|
|
405
|
+
prependAny(),
|
|
406
|
+
...mapProduct(appendWith, LETTERS),
|
|
407
|
+
appendAny(),
|
|
408
|
+
...mapProduct(insertWith, LETTERS),
|
|
409
|
+
insertAny(),
|
|
410
|
+
behead(),
|
|
411
|
+
curtail(),
|
|
412
|
+
...mapProduct(deleteWith, LETTERS),
|
|
413
|
+
deleteAny(),
|
|
414
|
+
takeOddOrEven(),
|
|
415
|
+
...mapProduct(changeTo, LETTERS),
|
|
416
|
+
changeAny(),
|
|
417
|
+
reverse(),
|
|
418
|
+
rotate(),
|
|
419
|
+
swapAdjacent(),
|
|
420
|
+
swapEnds(),
|
|
421
|
+
anagram(),
|
|
422
|
+
...mapProduct(transaddWith, LETTERS),
|
|
423
|
+
transaddAny(),
|
|
424
|
+
...mapProduct(transdeleteWith, LETTERS),
|
|
425
|
+
transdeleteAny(),
|
|
426
|
+
caesarShift(),
|
|
427
|
+
];
|
|
428
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Distribution } from "./distribution.js";
|
|
2
|
+
import { LogNum } from "./logNum.js";
|
|
3
|
+
import { memoize } from "./memoize.js";
|
|
4
|
+
import { interval } from "./util.js";
|
|
5
|
+
|
|
6
|
+
class BaseAffixDistribution {
|
|
7
|
+
/** Map from affix length to distribution of affixes of that length. */
|
|
8
|
+
private readonly dist = new Map<number, Distribution<string>>();
|
|
9
|
+
|
|
10
|
+
constructor(affix: "prefix" | "suffix", wordlist: string[]) {
|
|
11
|
+
let maxLength = 0;
|
|
12
|
+
for (const word of wordlist) {
|
|
13
|
+
maxLength = Math.max(maxLength, word.length);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const affixes = new Map<number, string[]>();
|
|
17
|
+
|
|
18
|
+
for (const i of interval(1, maxLength)) {
|
|
19
|
+
affixes.set(i, []);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const word of wordlist) {
|
|
23
|
+
for (let i = 1; i <= word.length; i++) {
|
|
24
|
+
affixes
|
|
25
|
+
.get(i)!
|
|
26
|
+
.push(affix === "prefix" ? word.slice(0, i) : word.slice(-i));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const [length, items] of affixes) {
|
|
31
|
+
this.dist.set(length, Distribution.from(items));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Distribution of affixes of a given length. */
|
|
36
|
+
get(length: number): Distribution<string> {
|
|
37
|
+
return this.dist.get(length) ?? Distribution.from([]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Vowel distribution for affixes of a given length. */
|
|
41
|
+
@memoize(1)
|
|
42
|
+
private vowelDist(length: number): Distribution<string> {
|
|
43
|
+
return this.get(length).map((s) => {
|
|
44
|
+
return s.replaceAll(/[aeiou]/g, "V").replaceAll(/[a-z]/g, "C");
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Probability that k affixes of a given length start with the same vowel
|
|
50
|
+
* pattern.
|
|
51
|
+
*/
|
|
52
|
+
@memoize(2)
|
|
53
|
+
probEqualVowelPattern(k: number, length: number): LogNum {
|
|
54
|
+
return this.vowelDist(length).probEqual(k);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Info about the prefix distribution of a wordlist. */
|
|
59
|
+
export class PrefixDistribution extends BaseAffixDistribution {
|
|
60
|
+
constructor(wordlist: string[]) {
|
|
61
|
+
super("prefix", wordlist);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Info about the suffix distribution of a wordlist. */
|
|
66
|
+
export class SuffixDistribution extends BaseAffixDistribution {
|
|
67
|
+
constructor(wordlist: string[]) {
|
|
68
|
+
super("suffix", wordlist);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** A map from items to counts. */
|
|
2
|
+
export class Counter<T extends PropertyKey> {
|
|
3
|
+
private readonly counts: Map<T, number>;
|
|
4
|
+
|
|
5
|
+
constructor(counts?: Map<T, number>, totalCache?: number) {
|
|
6
|
+
this.counts = counts ?? new Map<T, number>();
|
|
7
|
+
this.totalCache = totalCache;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static from(data: string): Counter<string>;
|
|
11
|
+
static from<T extends PropertyKey>(data: Iterable<T>): Counter<T>;
|
|
12
|
+
static from(data: string | Iterable<PropertyKey>) {
|
|
13
|
+
const counts = new Map<PropertyKey, number>();
|
|
14
|
+
let total = 0;
|
|
15
|
+
|
|
16
|
+
for (const item of data) {
|
|
17
|
+
counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
18
|
+
total += 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return new Counter(counts, total);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Add the given item to the counter. */
|
|
25
|
+
addOne(item: T): void {
|
|
26
|
+
this.counts.set(item, (this.counts.get(item) ?? 0) + 1);
|
|
27
|
+
if (this.totalCache !== undefined) {
|
|
28
|
+
this.totalCache += 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Add the given items to the counter. */
|
|
33
|
+
addMany(data: Iterable<T>): void {
|
|
34
|
+
this.totalCache = undefined;
|
|
35
|
+
for (const item of data) {
|
|
36
|
+
this.counts.set(item, (this.counts.get(item) ?? 0) + 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The number of distinct items. */
|
|
41
|
+
get distinct(): number {
|
|
42
|
+
return this.counts.size;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private totalCache: number | undefined;
|
|
46
|
+
|
|
47
|
+
/** The total number of all items. */
|
|
48
|
+
get total(): number {
|
|
49
|
+
return (this.totalCache ??= Array.from(this.counts.values()).reduce(
|
|
50
|
+
(a, b) => a + b,
|
|
51
|
+
0,
|
|
52
|
+
));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The count of the given item. */
|
|
56
|
+
get(item: T): number {
|
|
57
|
+
return this.counts.get(item) ?? 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Returns an iterable of [item, count] pairs. */
|
|
61
|
+
entries(): IterableIterator<[T, number]> {
|
|
62
|
+
return this.counts.entries();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Returns a list of items that satisfy the given predicate. */
|
|
66
|
+
filterKeys(fn: (item: T, count: number) => boolean): T[] {
|
|
67
|
+
return Array.from(this.counts.entries())
|
|
68
|
+
.filter(([item, count]) => fn(item, count))
|
|
69
|
+
.map(([item]) => item);
|
|
70
|
+
}
|
|
71
|
+
}
|