Skip to main content

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}