-
Notifications
You must be signed in to change notification settings - Fork 0
/
vietrandsylls.ts
292 lines (245 loc) · 7.86 KB
/
vietrandsylls.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
/*
* creation: unknown
*
* Creates random syllables that look Vietnamese
* yeah
*/
/**
* Orthographical units that make up a syllable
*/
namespace Unit {
/** The possible initial consonants */
export const Initials = [
'', 'b', 'c', 'ch', 'd', 'đ', 'g', 'gi', 'h', 'kh', 'l', 'm',
'n', 'ng', 'nh', 'ph', 'r', 's', 't', 'th', 'tr', 'v', 'x'
] as const;
export type Initial = typeof Initials[number];
/** The possible final consonants */
export const Finals = [
'', 'c', 'ch', 'm', 'n', 'ng', 'nh', 'p', 't'
] as const;
export type Final = typeof Finals[number];
/** The possible vowels */
export const Vowels = [
"a", "ă", "â", "e", "ê", "o", "ô", "ơ", "u", "ư", // normal monoph
"i", "iê", "ươ", "uô", // [i, y], [iê, ia], [ươ, ưa], [uô, ua]
] as const;
export type Vowel = typeof Vowels[number];
/** The possible combination of on-glides and off-glides */
export const Glides = [
"_", "w_", "w_w", "w_j", "_j", "_w"
] as const;
export type Glide = typeof Glides[number];
/** The possible tones */
export const Tones = [
'', '\u0300', '\u0301', '\u0303', '\u0309', '\u0323'
] as const;
export type Tone = typeof Tones[number];
}
/**
* Utility namespace to manipulate probability weights
*/
namespace Weights {
/**
* A weight, either an integer (which represents a weight) or a probability
*/
export type Weight =
| number
| RelWeight;
/**
* Weight dependent on the total weight of the set of weights
*/
interface RelWeight {
/**
* The probability that this item occurs.
* This should be a number between [0, 1]
* and the total fraction of relative weights should be no greater than 1.
*/
fr: number
};
/**
* Create a uniform distribution of weights from a list.
*/
function uniform<T extends string>(t: readonly T[]): Record<T, number> {
return fromEntries(t.map(e => [e, 1] as const));
}
/**
* Convert a record of weights (which may include relative weights) into purely absolute weights
* @param weights record of weights
* @returns record of absolute weights
*/
export function absWeights<T extends PropertyKey>(weights: Record<T, Weight>): Record<T, number> {
const r = { ...weights };
const absolutes: number[] = [];
const relatives: RelWeight[] = [];
for (let w of Object.values<Weight>(weights)) {
if (typeof w === "number") absolutes.push(w);
else relatives.push(w);
}
let total: number;
if (absolutes.length !== 0) {
const absSum = absolutes.reduce((acc, cv) => acc + cv, 0);
const absPerc = 1 - relatives.reduce((acc, {fr}) => acc + fr, 0);
total = absSum / absPerc;
} else {
total = 1 / relatives.reduce((acc, cv) => acc * cv.fr, 1);
}
for (let [k, w] of Object.entries<Weight>(r)) {
if (typeof w !== "number") r[k as T] = Math.round(w.fr * total);
}
return r as any;
}
/**
* Weights of final consonants
*/
export const wFinals: Record<Unit.Final, Weight> = {
...uniform(Unit.Finals),
"": { fr: 1/2 }
};
/**
* Weights of glides
*/
export const wGlides: Record<Unit.Glide, Weight> = {
...uniform(Unit.Glides),
"_": { fr: 1/2 }
};
}
/**
* Choose uniformly from an array
* @param arr array
* @returns an element
*/
function choose<T>(arr: ArrayLike<T>) {
const i = Math.floor(Math.random() * arr.length);
return arr[i];
}
/**
* Choose from a record of weights
* @param weights record of weights
* @returns an element
*/
function chooseWeighted<T extends PropertyKey>(weights: Record<T, Weights.Weight>) {
const entries = Object.entries<number>(Weights.absWeights(weights)) as [T, number][];
const sum = entries.map(([_, w]) => w)
.reduce((acc, cv) => acc + cv, 0);
let i = Math.floor(Math.random() * sum);
for (let [k, w] of entries) {
i -= w;
if (i < 0) return k;
}
return entries[entries.length - 1][0];
}
/**
* Make a copy of an object with some keys omitted
* @param o Object
* @param omit Keys to omit
* @returns new object
*/
function omit<T extends PropertyKey, U extends T, V>(o: Record<T, V>, omit: U[]): Omit<typeof o, U> {
const result = { ...o };
for (let u of omit) delete result[u];
return result;
}
/**
* Object.fromEntries that types into a Record
*/
function fromEntries<T extends PropertyKey, V>(entries: readonly (readonly [T, V])[]): Record<T, V> {
return Object.fromEntries<V>(entries) as Record<T, V>;
}
/**
* Split a glide into an on-glide and an off-glide
*/
function splitGlide<G extends Unit.Glide>(g: G): G extends `${infer S1}_${infer S2}` ? [S1, S2] : never {
return g.split("_") as any;
};
/**
* Randomly choose a glide
*/
function chooseGlide(v: Unit.Vowel): Unit.Glide {
const glides: Unit.Glide[] = ["_"];
if (!'ưươoôuuô'.includes(v)) glides.push('w_');
if ('eia'.includes(v)) glides.push('w_w');
if ('aăâ'.includes(v)) glides.push('w_j');
if (!'eêiiê'.includes(v)) glides.push('_j');
if (!'oôuuô'.includes(v)) glides.push('_w');
const weights = fromEntries(
glides.map(g => [g, Weights.wGlides[g]])
);
return chooseWeighted(weights);
}
/**
* Randomly choose a final consonant
*/
function chooseFinal(v: Unit.Vowel, g: Unit.Glide): Unit.Final {
if (!g.endsWith("_")) return "";
const finals = "aêi".includes(v) ?
Weights.wFinals
: omit(Weights.wFinals, ["ch", "nh"]);
return chooseWeighted(finals);
}
function chooseSylConfig():
[Unit.Initial, Unit.Vowel, Unit.Glide, Unit.Final, Unit.Tone] {
const initial = choose(Unit.Initials);
const vowel = choose(Unit.Vowels);
const glide = chooseGlide(vowel);
const final = chooseFinal(vowel, glide);
const tone = choose(Unit.Tones);
return [initial, vowel, glide, final, tone];
}
export function generateSyllable() {
const [initial, vowel, glide, final, tone] = chooseSylConfig();
let [onGlide, offGlide] = splitGlide(glide);
// apply off-glide
// ăj => ay
// âj => ây
// _j => _i
// aw => ao
// ew => eo
// ăw => au
// _w => _u
let syllable = `${vowel}${tone}${offGlide}`
.replace("j", () => "ăâ".includes(vowel) ? "y" : "i")
.replace("w", () => "ae".includes(vowel) ? "o" : "u")
.replace(/ă(\p{M}?)y/u, "a$1y")
.replace(/ă(\p{M}?)w/u, "a$1w");
// apply on-glide
// cw_ => qu_
// wa => oa
// wă => oă
// we => oe
// wi => uy
// w_ => u_
let dInitial: string = initial;
if (onGlide === "w") {
if (dInitial === "c") {
dInitial = "q";
syllable = `u${syllable}`;
} else {
syllable = `${onGlide}${syllable}`
.replace("w", () => "aăe".includes(vowel) ? "o" : "u")
}
}
syllable.replace("ui", "uy");
// apply initial
// ci, ce, cê => ki, ke, kê
// (n)gi, (n)ge, (n)gê => (n)ghi, (n)ghe, (n)ghê
// ^iê => yê
// gi + i => gi
syllable = `${dInitial}${syllable}`
.replace(/c([eêi])/, "k$1")
.replace(/gii/, "gi")
.replace(/g([eêi])/, "gh$1")
.replace(/^iê/, "yê");
// apply final
// iê$, ươ$, uô$ => ia, ưa, ua
// for tones, ia, ưa, ua => [i]a, [ư]a, [u]a
syllable = `${syllable}${final}`
.replace(/iê(\p{M}?)$/u, "i$1a")
.replace(/ươ(\p{M}?)$/u, "ư$1a")
.replace(/uô(\p{M}?)$/u, "u$1a");
return syllable.normalize("NFC");
}
export function generateWord() {
const length = choose([1,1,1,2,2,3]);
return Array.from({length}, () => generateSyllable()).join(" ");
}