keyhammer/cost.rs
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 Robson Trasel
3
4//! Fixed-point edit costs (unit = 16) and keyboard-layout adjacency.
5//!
6//! Integer costs make the search bounds exact and the results identical on
7//! every platform. All values below are provisional until the calibration
8//! phase.
9
10/// A fixed-point edit cost (16 = one ordinary edit).
11pub type Cost = u16;
12
13/// The cost of one ordinary edit. Every cost in the table below is expressed
14/// in these units: cheaper edits (a neighbouring key, a doubled letter, a
15/// transposition) cost a fraction of it, edits on the first byte cost more.
16pub const COST_UNIT: Cost = 16;
17
18/// The weighted `cost` rounded up to whole units of [`COST_UNIT`]: 0 stays 0,
19/// 1 to 16 is one unit, 17 to 32 two units, and so on.
20///
21/// This is not a count of edits. The x1.5 factor on the first query byte
22/// makes an ordinary (non-neighbouring-key) edit there cost 24, i.e. two
23/// units, while a neighbouring-key substitution there costs 12 (one unit) and
24/// a transposition 18 (two units); two cheap edits (8 + 8) count as one.
25#[inline]
26pub fn whole_units(cost: Cost) -> Cost {
27 cost.div_ceil(COST_UNIT)
28}
29
30/// Sentinel for "unreachable or over budget". `INF` plus any single edit cost
31/// still fits in a `u16`.
32pub const INF: Cost = 30_000;
33
34/// Bit that represents the character class of symbol `s` (used by subtree
35/// signatures). Symbols are Unicode scalar values (see `docs/design/unicode.md`).
36///
37/// - ASCII (`s < 128`): bit `s & 63`, so ASCII characters that agree modulo 64
38/// share a class (`a` (97) and `!` (33), for instance, or digits and `p`-`y`).
39/// The letters `a`-`z` take the classes 33 to 58 and are distinct from one
40/// another.
41/// - Everything else: one of the 38 classes the letters `a`-`z` never use (0 to
42/// 32 and 59 to 63), chosen by `s % 38`, so that an accented letter or a
43/// letter of another script never hides a missing `a`-`z` letter.
44///
45/// The collisions are harmless: the signatures only bound the search, and a
46/// collision can make a bound weaker (a class looks present when it is not),
47/// never wrong. The proof needs only that equal symbols have equal classes.
48///
49/// # Examples
50///
51/// ```
52/// use keyhammer::cost::symbol_class;
53///
54/// assert_ne!(symbol_class('a'.into()), symbol_class('b'.into()));
55/// assert_eq!(symbol_class('a'.into()), symbol_class('!'.into())); // 97 and 33, modulo 64
56/// // No non-ASCII symbol shares a class with a letter a-z.
57/// let letters = ('a'..='z').fold(0, |m, c| m | symbol_class(c.into()));
58/// assert_eq!(symbol_class('é'.into()) & letters, 0);
59/// ```
60#[inline]
61pub fn symbol_class(s: u32) -> u64 {
62 /// The classes that no letter `a`-`z` uses.
63 const FREE: [u8; 38] = [
64 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
65 25, 26, 27, 28, 29, 30, 31, 32, 59, 60, 61, 62, 63,
66 ];
67 if s < 128 {
68 1u64 << (s & 63)
69 } else {
70 1u64 << FREE[(s % 38) as usize]
71 }
72}
73
74/// The class of the byte `b` read as the code point U+0000 to U+00FF: the same
75/// as [`symbol_class`]`(b.into())`. For ASCII that is bit `b & 63`.
76///
77/// # Examples
78///
79/// ```
80/// use keyhammer::cost::class;
81///
82/// assert_ne!(class(b'a'), class(b'b'));
83/// assert_eq!(class(b'a'), class(b'!')); // 97 and 33 are equal modulo 64
84/// ```
85#[inline]
86pub fn class(b: u8) -> u64 {
87 symbol_class(u32::from(b))
88}
89
90/// One row of a keyboard [`Layout`]: the keys left to right and where the
91/// first one starts.
92///
93/// Positions are in half key widths, so key `i` is at `offset + 2 * i`. Every
94/// key takes part in the cost model, letters `a` to `z` and others (`ç`, `ù`,
95/// `;`, ...) alike.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub(crate) struct Row {
98 offset: u8,
99 keys: &'static str,
100}
101
102impl Row {
103 #[cfg(test)]
104 fn keys(&self) -> &'static str {
105 self.keys
106 }
107}
108
109const fn row(offset: u8, keys: &'static str) -> Row {
110 Row { offset, keys }
111}
112
113const QWERTY: [Row; 3] = [row(0, "qwertyuiop"), row(1, "asdfghjkl"), row(2, "zxcvbnm")];
114const QWERTZ: [Row; 3] = [
115 row(0, "qwertzuiopü"),
116 row(1, "asdfghjklöä"),
117 row(2, "yxcvbnm"),
118];
119const AZERTY: [Row; 3] = [
120 row(0, "azertyuiop"),
121 row(1, "qsdfghjklmù"),
122 row(2, "wxcvbn"),
123];
124const ABNT2: [Row; 3] = [
125 row(0, "qwertyuiop"),
126 row(1, "asdfghjklç"),
127 row(2, "zxcvbnm,.;"),
128];
129const DVORAK: [Row; 3] = [
130 row(0, "',.pyfgcrl"),
131 row(1, "aoeuidhtns"),
132 row(2, ";qjkxbmwvz"),
133];
134const COLEMAK: [Row; 3] = [
135 row(0, "qwfpgjluy;"),
136 row(1, "arstdhneio"),
137 row(2, "zxcvbkm"),
138];
139
140/// Walks every unordered pair of neighbouring keys of `rows` (the rule
141/// described on [`Layout`]), at compile time. Pairs of two letters `a`-`z`
142/// go into the bit sets `adj`; every other pair is written to `extra` while
143/// it has room. Returns the number of other pairs, written or not.
144const fn walk(rows: &[Row; 3], adj: &mut [u32; 26], extra: &mut [(u32, u32)]) -> usize {
145 let mut n = 0;
146 let mut r = 0;
147 while r < 3 {
148 let a_row = rows[r].keys.as_bytes();
149 let mut i = 0; // key index (a character, not a byte)
150 let mut ba = 0; // byte index of key i
151 while ba < a_row.len() {
152 let xa = rows[r].offset as usize + 2 * i;
153 let ka = decode_key(a_row, ba);
154 // Next key in the same row.
155 let bn = next_key(a_row, ba);
156 if bn < a_row.len() {
157 n = link(adj, extra, n, ka, decode_key(a_row, bn));
158 }
159 if r + 1 < 3 {
160 let b_row = rows[r + 1].keys.as_bytes();
161 let mut j = 0;
162 let mut bb = 0;
163 while bb < b_row.len() {
164 let xb = rows[r + 1].offset as usize + 2 * j;
165 if xa.abs_diff(xb) <= 1 {
166 n = link(adj, extra, n, ka, decode_key(b_row, bb));
167 }
168 bb = next_key(b_row, bb);
169 j += 1;
170 }
171 }
172 ba = bn;
173 i += 1;
174 }
175 r += 1;
176 }
177 n
178}
179
180/// Neighbour bit sets of the letters `a`-`z` of one layout.
181const fn table(rows: &[Row; 3]) -> [u32; 26] {
182 let mut adj = [0u32; 26];
183 walk(rows, &mut adj, &mut []);
184 adj
185}
186
187/// Number of neighbour pairs of one layout that involve a key outside `a`-`z`.
188const fn extra_count(rows: &[Row; 3]) -> usize {
189 walk(rows, &mut [0u32; 26], &mut [])
190}
191
192/// The neighbour pairs of one layout that involve a key outside `a`-`z`.
193const fn extra<const N: usize>(rows: &[Row; 3]) -> [(u32, u32); N] {
194 let mut out = [(0u32, 0u32); N];
195 walk(rows, &mut [0u32; 26], &mut out);
196 out
197}
198
199/// The code point of the UTF-8 key starting at byte `b`.
200const fn decode_key(bytes: &[u8], b: usize) -> u32 {
201 let x = bytes[b] as u32;
202 if x < 0x80 {
203 x
204 } else if x < 0xE0 {
205 ((x & 0x1F) << 6) | cont(bytes, b + 1)
206 } else if x < 0xF0 {
207 ((x & 0x0F) << 12) | (cont(bytes, b + 1) << 6) | cont(bytes, b + 2)
208 } else {
209 ((x & 0x07) << 18)
210 | (cont(bytes, b + 1) << 12)
211 | (cont(bytes, b + 2) << 6)
212 | cont(bytes, b + 3)
213 }
214}
215
216/// The six payload bits of the UTF-8 continuation byte at `i`.
217const fn cont(bytes: &[u8], i: usize) -> u32 {
218 (bytes[i] & 0x3F) as u32
219}
220
221/// Byte index of the key after the one starting at `b` (keys are UTF-8).
222const fn next_key(bytes: &[u8], b: usize) -> usize {
223 let mut n = b + 1;
224 while n < bytes.len() && bytes[n] & 0xC0 == 0x80 {
225 n += 1;
226 }
227 n
228}
229
230/// Records the pair `a`, `b`: in `adj` if both are letters `a`-`z`, else as
231/// pair number `n` in `extra` (if there is room). Returns the new count of
232/// other pairs.
233const fn link(adj: &mut [u32; 26], extra: &mut [(u32, u32)], n: usize, a: u32, b: u32) -> usize {
234 let (ia, ib) = (a.wrapping_sub(0x61), b.wrapping_sub(0x61));
235 if ia < 26 && ib < 26 {
236 adj[ia as usize] |= 1 << ib;
237 adj[ib as usize] |= 1 << ia;
238 return n;
239 }
240 if n < extra.len() {
241 extra[n] = (a, b);
242 }
243 n + 1
244}
245
246const QWERTY_ADJ: [u32; 26] = table(&QWERTY);
247const QWERTZ_ADJ: [u32; 26] = table(&QWERTZ);
248const AZERTY_ADJ: [u32; 26] = table(&AZERTY);
249const ABNT2_ADJ: [u32; 26] = table(&ABNT2);
250const DVORAK_ADJ: [u32; 26] = table(&DVORAK);
251const COLEMAK_ADJ: [u32; 26] = table(&COLEMAK);
252
253const QWERTY_EXTRA: [(u32, u32); extra_count(&QWERTY)] = extra(&QWERTY);
254const QWERTZ_EXTRA: [(u32, u32); extra_count(&QWERTZ)] = extra(&QWERTZ);
255const AZERTY_EXTRA: [(u32, u32); extra_count(&AZERTY)] = extra(&AZERTY);
256const ABNT2_EXTRA: [(u32, u32); extra_count(&ABNT2)] = extra(&ABNT2);
257const DVORAK_EXTRA: [(u32, u32); extra_count(&DVORAK)] = extra(&DVORAK);
258const COLEMAK_EXTRA: [(u32, u32); extra_count(&COLEMAK)] = extra(&COLEMAK);
259
260/// A physical keyboard layout, from which the neighbouring-key relation of a
261/// [`CostModel`] is derived.
262///
263/// # Geometry
264///
265/// Only the three letter rows are modelled (top, home, bottom). Every key is
266/// one unit wide and rows are shifted by half keys: offsets of 0, 0.5 and 1
267/// key widths for every layout here. That rounds the real stagger of an ANSI
268/// board (0, 0.25 and 0.75) so that the derived relation equals the one QWERTY
269/// has always had in this crate. Two keys are neighbours when
270///
271/// - they are next to each other in a row, or
272/// - they are in consecutive rows and their centres are at most half a key
273/// apart (a key touches two keys of the next row, or one at the end of a
274/// row).
275///
276/// The relation is symmetric and irreflexive by construction.
277///
278/// # What is and is not covered
279///
280/// Every modelled key counts, letters `a` to `z` and others alike: `ç` on
281/// ABNT2 (next to `l` and `p`, and to `.` and `;` below it), `ù` on AZERTY
282/// (after `m`), `ü` on QWERTZ (after `p`) and `ö`, `ä` (after `l`), and the
283/// punctuation keys of the ABNT2 bottom row, of Dvorak and of Colemak. The
284/// search compares Unicode code points (`docs/design/unicode.md`), so a
285/// query with `ç` for a term with `l` is a neighbouring-key substitution on
286/// ABNT2. Keys are lowercase: an uppercase letter is a different symbol,
287/// with no neighbours, unless the text is case folded (see
288/// [`crate::text::Normalizer`]). With diacritic folding on (the default),
289/// `ç`, `ù`, `ü`, `ö` and `ä` never reach the search (they become `c`, `u`,
290/// `u`, `o` and `a`), so these neighbours only matter without it.
291///
292/// Not modelled at all: the number row, the extra ISO key beside the left
293/// shift, the punctuation columns after the letter rows of QWERTY, QWERTZ and
294/// AZERTY and after the top row of ABNT2, modifiers, dead keys and AltGr
295/// layers. The unit costs are the same for every layout and remain
296/// provisional.
297///
298/// The pairs of two letters `a`-`z` of ABNT2 are those of QWERTY; the layouts
299/// differ by `ç` and the punctuation keys.
300///
301/// # Examples
302///
303/// ```
304/// use keyhammer::cost::Layout;
305///
306/// assert!(Layout::Qwerty.are_neighbours('a', 's'));
307/// assert!(!Layout::Qwerty.are_neighbours('a', 'd'));
308/// // ç sits next to l and p on ABNT2.
309/// assert!(Layout::Abnt2.are_neighbours('l', 'ç'));
310/// assert!(Layout::Abnt2.are_neighbours('ç', 'p'));
311/// // ù follows m on AZERTY, ö follows l on QWERTZ.
312/// assert!(Layout::Azerty.are_neighbours('m', 'ù'));
313/// assert!(Layout::Qwertz.are_neighbours('l', 'ö'));
314/// // QWERTZ swaps y and z: z now sits beside t, u, g and h.
315/// assert!(Layout::Qwertz.are_neighbours('z', 't'));
316/// assert!(!Layout::Qwerty.are_neighbours('z', 't'));
317/// // Dvorak: home row is a o e u i d h t n s.
318/// assert!(Layout::Dvorak.are_neighbours('u', 'i'));
319/// // Colemak: home row is a r s t d h n e i o.
320/// assert!(Layout::Colemak.are_neighbours('r', 's'));
321/// ```
322#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
323#[non_exhaustive]
324pub enum Layout {
325 /// ANSI QWERTY, the default.
326 Qwerty,
327 /// QWERTZ (German): `y` and `z` swapped relative to QWERTY, `ü` after `p`
328 /// and `ö`, `ä` after `l`.
329 Qwertz,
330 /// AZERTY (French): `a`/`q` and `z`/`w` swapped, `m` and `ù` on the home
331 /// row.
332 Azerty,
333 /// Brazilian ABNT2: the QWERTY letters, plus `ç` after `l` and the
334 /// punctuation keys `,` `.` `;` after `m`.
335 Abnt2,
336 /// Dvorak (US).
337 Dvorak,
338 /// Colemak (US).
339 Colemak,
340}
341
342impl Layout {
343 /// Every layout known to this version of the crate.
344 pub const ALL: &'static [Layout] = &[
345 Layout::Qwerty,
346 Layout::Qwertz,
347 Layout::Azerty,
348 Layout::Abnt2,
349 Layout::Dvorak,
350 Layout::Colemak,
351 ];
352
353 /// Lowercase name of the layout.
354 ///
355 /// ```
356 /// assert_eq!(keyhammer::cost::Layout::Abnt2.name(), "abnt2");
357 /// ```
358 pub fn name(self) -> &'static str {
359 match self {
360 Layout::Qwerty => "qwerty",
361 Layout::Qwertz => "qwertz",
362 Layout::Azerty => "azerty",
363 Layout::Abnt2 => "abnt2",
364 Layout::Dvorak => "dvorak",
365 Layout::Colemak => "colemak",
366 }
367 }
368
369 /// The letter rows of the layout, top to bottom.
370 pub(crate) fn rows(self) -> &'static [Row] {
371 match self {
372 Layout::Qwerty => &QWERTY,
373 Layout::Qwertz => &QWERTZ,
374 Layout::Azerty => &AZERTY,
375 Layout::Abnt2 => &ABNT2,
376 Layout::Dvorak => &DVORAK,
377 Layout::Colemak => &COLEMAK,
378 }
379 }
380
381 /// Whether keys `a` and `b` are neighbours on this layout. `false` for a
382 /// key that is not on the modelled rows and for `a == b`. The cost model
383 /// of [`CostModel::for_layout`] uses exactly this relation.
384 ///
385 /// ```
386 /// use keyhammer::cost::Layout;
387 ///
388 /// assert!(Layout::Azerty.are_neighbours('q', 'z')); // not so on QWERTY
389 /// assert!(!Layout::Qwerty.are_neighbours('q', 'z'));
390 /// assert!(!Layout::Qwerty.are_neighbours('q', 'q'));
391 /// ```
392 pub fn are_neighbours(self, a: char, b: char) -> bool {
393 let mut found = false;
394 self.for_each_pair(|x, y| found |= (x == a && y == b) || (x == b && y == a));
395 found
396 }
397
398 /// Calls `f` once for every unordered pair of neighbouring keys.
399 fn for_each_pair(self, mut f: impl FnMut(char, char)) {
400 let rows = self.rows();
401 for (r, row) in rows.iter().enumerate() {
402 for (i, a) in row.keys.chars().enumerate() {
403 let xa = usize::from(row.offset) + 2 * i;
404 if let Some(b) = row.keys.chars().nth(i + 1) {
405 f(a, b);
406 }
407 if let Some(below) = rows.get(r + 1) {
408 for (j, b) in below.keys.chars().enumerate() {
409 if xa.abs_diff(usize::from(below.offset) + 2 * j) <= 1 {
410 f(a, b);
411 }
412 }
413 }
414 }
415 }
416 }
417}
418
419/// Costs of the four edit operations.
420///
421/// Built for a keyboard [`Layout`] with [`CostModel::for_layout`]
422/// ([`CostModel::qwerty`] is the default). The layout only decides which
423/// substitutions are cheap; the minimum costs the search bounds use
424/// ([`c_min`](Self::c_min), [`c_indel_min`](Self::c_indel_min),
425/// [`c_transpose_min`](Self::c_transpose_min)) do not depend on it.
426#[derive(Clone, Debug, PartialEq, Eq)]
427pub struct CostModel {
428 sub: Cost,
429 sub_adjacent: Cost,
430 indel: Cost,
431 indel_double: Cost,
432 transpose: Cost,
433 /// Neighbour bit sets of the letters `a`-`z`.
434 adjacent: [u32; 26],
435 /// Neighbour pairs that involve a key outside `a`-`z`.
436 extra: &'static [(u32, u32)],
437}
438
439/// Edits on the first query byte are less likely, so they cost 1.5x.
440#[inline]
441fn at_start(cost: Cost, qpos: usize) -> Cost {
442 if qpos == 0 { cost + cost / 2 } else { cost }
443}
444
445impl CostModel {
446 /// The default model with a QWERTY keyboard. Same as
447 /// `CostModel::for_layout(Layout::Qwerty)`.
448 pub fn qwerty() -> Self {
449 Self::for_layout(Layout::Qwerty)
450 }
451
452 /// A model whose cheap substitutions are the neighbouring keys of
453 /// `layout` ([`Layout::are_neighbours`]); see [`Layout`] for what the
454 /// geometry leaves out. Every other pair of characters costs an ordinary
455 /// substitution. Does not panic.
456 ///
457 /// # Examples
458 ///
459 /// ```
460 /// use keyhammer::cost::{CostModel, Layout};
461 ///
462 /// let qwerty = CostModel::for_layout(Layout::Qwerty);
463 /// let azerty = CostModel::for_layout(Layout::Azerty);
464 /// // q and z touch on AZERTY, not on QWERTY.
465 /// assert_eq!(azerty.sub_cost(b'q', b'z', 1), 8);
466 /// assert_eq!(qwerty.sub_cost(b'q', b'z', 1), 16);
467 /// // m is next to n on QWERTY only.
468 /// assert_eq!(qwerty.sub_cost(b'm', b'n', 1), 8);
469 /// assert_eq!(azerty.sub_cost(b'm', b'n', 1), 16);
470 /// // ABNT2 has the QWERTY letters, and ç next to l and p.
471 /// let abnt2 = CostModel::for_layout(Layout::Abnt2);
472 /// assert_eq!(abnt2.sub_cost('ç', 'l', 1), 8);
473 /// assert_eq!(qwerty.sub_cost('ç', 'l', 1), 16);
474 /// assert_eq!(abnt2.sub_cost('a', 's', 1), qwerty.sub_cost('a', 's', 1));
475 /// ```
476 pub fn for_layout(layout: Layout) -> Self {
477 let (adjacent, extra): ([u32; 26], &'static [(u32, u32)]) = match layout {
478 Layout::Qwerty => (QWERTY_ADJ, &QWERTY_EXTRA),
479 Layout::Qwertz => (QWERTZ_ADJ, &QWERTZ_EXTRA),
480 Layout::Azerty => (AZERTY_ADJ, &AZERTY_EXTRA),
481 Layout::Abnt2 => (ABNT2_ADJ, &ABNT2_EXTRA),
482 Layout::Dvorak => (DVORAK_ADJ, &DVORAK_EXTRA),
483 Layout::Colemak => (COLEMAK_ADJ, &COLEMAK_EXTRA),
484 };
485 Self {
486 sub: 16,
487 sub_adjacent: 8,
488 indel: 16,
489 indel_double: 8,
490 transpose: 12,
491 adjacent,
492 extra,
493 }
494 }
495
496 #[inline]
497 fn is_adjacent(&self, a: u32, b: u32) -> bool {
498 let (ia, ib) = (a.wrapping_sub(0x61), b.wrapping_sub(0x61));
499 if ia < 26 && ib < 26 {
500 return self.adjacent[ia as usize] & (1u32 << ib) != 0;
501 }
502 self.extra
503 .iter()
504 .any(|&(x, y)| (x == a && y == b) || (x == b && y == a))
505 }
506
507 /// Cost of typing `q` where the term has `t`; `qpos` is the index of `q` in the query.
508 ///
509 /// Symbols are Unicode scalar values: a `u8` is read as U+0000 to U+00FF,
510 /// a `char` or a `u32` as itself.
511 #[inline]
512 pub fn sub_cost<S: Into<u32>>(&self, q: S, t: S, qpos: usize) -> Cost {
513 let (q, t) = (q.into(), t.into());
514 if q == t {
515 return 0;
516 }
517 let base = if self.is_adjacent(q, t) {
518 self.sub_adjacent
519 } else {
520 self.sub
521 };
522 at_start(base, qpos)
523 }
524
525 /// Cost of deleting the query symbol at `qpos` (the user typed an extra one).
526 #[inline]
527 pub fn del_cost<S: PartialEq>(&self, q: &[S], qpos: usize) -> Cost {
528 let doubled =
529 qpos > 0 && matches!((q.get(qpos), q.get(qpos - 1)), (Some(a), Some(b)) if a == b);
530 at_start(
531 if doubled {
532 self.indel_double
533 } else {
534 self.indel
535 },
536 qpos,
537 )
538 }
539
540 /// Cost of inserting term symbol `t` (the user skipped it) when `qpos` query symbols are consumed.
541 #[inline]
542 pub fn ins_cost<S: PartialEq>(&self, t: S, prev_t: Option<S>, qpos: usize) -> Cost {
543 let base = if prev_t == Some(t) {
544 self.indel_double
545 } else {
546 self.indel
547 };
548 at_start(base, qpos)
549 }
550
551 /// Cost of swapping two adjacent symbols, the first at `qpos` in the query.
552 #[inline]
553 pub fn transpose_cost(&self, qpos: usize) -> Cost {
554 at_start(self.transpose, qpos)
555 }
556
557 /// Smallest cost of any single edit.
558 pub fn c_min(&self) -> Cost {
559 self.sub
560 .min(self.sub_adjacent)
561 .min(self.indel)
562 .min(self.indel_double)
563 .min(self.transpose)
564 }
565
566 /// Smallest cost of an insertion or deletion.
567 pub fn c_indel_min(&self) -> Cost {
568 self.indel.min(self.indel_double)
569 }
570
571 /// Smallest cost of a transposition.
572 pub fn c_transpose_min(&self) -> Cost {
573 self.transpose
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580 use alloc::vec::Vec;
581
582 /// Runtime neighbour set of a-z pairs, from the same geometry.
583 fn runtime_table(layout: Layout) -> [u32; 26] {
584 let mut adj = [0u32; 26];
585 layout.for_each_pair(|a, b| {
586 if a.is_ascii_lowercase() && b.is_ascii_lowercase() {
587 let (ia, ib) = (a as usize - 'a' as usize, b as usize - 'a' as usize);
588 adj[ia] |= 1 << ib;
589 adj[ib] |= 1 << ia;
590 }
591 });
592 adj
593 }
594
595 #[test]
596 fn compile_time_tables_equal_the_runtime_geometry() {
597 for &layout in Layout::ALL {
598 let cm = CostModel::for_layout(layout);
599 assert_eq!(cm.adjacent, runtime_table(layout), "{}", layout.name());
600 let mut runtime = Vec::new();
601 layout.for_each_pair(|a, b| {
602 if !(a.is_ascii_lowercase() && b.is_ascii_lowercase()) {
603 runtime.push((u32::from(a), u32::from(b)));
604 }
605 });
606 assert_eq!(cm.extra, runtime.as_slice(), "{}", layout.name());
607 }
608 }
609
610 #[test]
611 fn the_cost_model_uses_exactly_the_layout_relation() {
612 for &layout in Layout::ALL {
613 let cm = CostModel::for_layout(layout);
614 let mut keys: Vec<char> = layout
615 .rows()
616 .iter()
617 .flat_map(|r| r.keys().chars())
618 .collect();
619 // Characters on no modelled key, and case variants of keys.
620 keys.extend(['A', 'Ç', 'é', '1', ' ', 'ж', '\u{0}']);
621 for &a in &keys {
622 for &b in &keys {
623 let want = if a == b {
624 0
625 } else if layout.are_neighbours(a, b) {
626 8
627 } else {
628 16
629 };
630 assert_eq!(cm.sub_cost(a, b, 1), want, "{} {a:?} {b:?}", layout.name());
631 }
632 }
633 }
634 }
635
636 #[test]
637 fn the_new_keys_have_their_neighbours() {
638 let pairs = |layout: Layout, key: char| -> Vec<char> {
639 let mut v: Vec<char> = layout
640 .rows()
641 .iter()
642 .flat_map(|r| r.keys().chars())
643 .filter(|&o| layout.are_neighbours(key, o))
644 .collect();
645 v.sort_unstable();
646 v
647 };
648 assert_eq!(pairs(Layout::Abnt2, 'ç'), ['.', ';', 'l', 'p']);
649 assert_eq!(pairs(Layout::Azerty, 'ù'), ['m']);
650 assert_eq!(pairs(Layout::Qwertz, 'ü'), ['p', 'ä', 'ö']);
651 assert_eq!(pairs(Layout::Qwertz, 'ö'), ['l', 'p', 'ä', 'ü']);
652 assert_eq!(pairs(Layout::Qwertz, 'ä'), ['ö', 'ü']);
653 assert!(CostModel::qwerty().extra.is_empty());
654 }
655
656 #[test]
657 fn every_layout_has_each_letter_once_and_every_key_a_neighbour() {
658 for &layout in Layout::ALL {
659 let mut count = [0u8; 26];
660 for r in layout.rows() {
661 for c in r.keys().chars().filter(char::is_ascii_lowercase) {
662 count[(c as u8 - b'a') as usize] += 1;
663 }
664 }
665 assert!(count.iter().all(|&n| n == 1), "{}", layout.name());
666 for r in layout.rows() {
667 for k in r.keys().chars() {
668 assert!(!layout.are_neighbours(k, k));
669 let any = layout
670 .rows()
671 .iter()
672 .flat_map(|r| r.keys().chars())
673 .any(|o| layout.are_neighbours(k, o));
674 assert!(any, "{} key {k}", layout.name());
675 }
676 }
677 }
678 }
679}