Skip to main content

chai/
lib.rs

1//! libchai 是使用 Rust 实现的汉字编码输入方案的优化算法。它同时发布为一个 Rust crate 和一个 NPM 模块,前者可以在 Rust 项目中安装为依赖来使用,后者可以通过汉字自动拆分系统的图形界面来使用。
2//!
3//! chai 是使用 libchai 实现的命令行程序,用户提供方案配置文件、拆分表和评测信息,本程序能够生成编码并评测一系列指标,以及基于退火算法优化元素的布局。
4
5pub mod config;
6pub mod contexts;
7pub mod encoders;
8pub mod interfaces;
9pub mod objectives;
10pub mod operators;
11pub mod optimizers;
12#[cfg(feature = "server")]
13pub mod server;
14
15use config::{安排, 广义码位};
16use objectives::metric::指法标记;
17use rustc_hash::FxHashMap;
18use serde::{Deserialize, Serialize};
19use std::cmp::Reverse;
20use std::io;
21use wasm_bindgen::JsError;
22
23use crate::config::条件;
24
25/// 只考虑长度为 1 到 10 的词
26pub const 最大词长: usize = 10;
27
28/// 只对低于最大按键组合长度的编码预先计算当量
29pub const 最大按键组合长度: usize = 4;
30
31#[derive(Debug, Serialize, Deserialize, Clone)]
32pub struct 原始元素序列及条件列表 {
33    pub 元素序列: Vec<广义码位>,
34    pub 条件列表: Vec<条件>,
35}
36
37/// 从配置文件中读取的原始可编码对象
38#[derive(Debug, Serialize, Deserialize, Clone)]
39pub struct 原始可编码对象 {
40    pub 词: String,
41    /// 简单格式:单个元素序列,无条件
42    #[serde(default)]
43    pub 元素序列: Option<Vec<广义码位>>,
44    /// 复杂格式:多个带条件的元素序列
45    #[serde(default)]
46    pub 全部元素序列: Option<Vec<原始元素序列及条件列表>>,
47    pub 频率: u64,
48    #[serde(default = "原始可编码对象::默认级别")]
49    pub 简码长度: u64,
50}
51
52impl 原始可编码对象 {
53    const fn 默认级别() -> u64 {
54        u64::MAX
55    }
56}
57
58pub type 原始键位分布信息 = FxHashMap<char, 键位分布损失函数>;
59pub type 键位分布信息 = Vec<键位分布损失函数>;
60pub type 原始当量信息 = FxHashMap<String, f64>;
61pub type 当量信息 = Vec<f64>;
62
63/// 键位分布的理想值和惩罚值
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct 键位分布损失函数 {
66    pub 理想值: f64,
67    pub 低于惩罚: f64,
68    pub 高于惩罚: f64,
69}
70
71/// 元素用一个无符号整数表示
72pub type 元素 = usize;
73
74/// 元素位 = 元素 + 位置 * 元素总数,位置从 0 开始,元素总数由棱镜决定。这样设计的好处是可以把元素和位置的信息都编码在一个整数中,方便处理和比较。
75pub type 元素位 = usize;
76
77/// 最大元素序列长度,超过这个长度的编码会被拒绝
78pub const 最大元素序列长度: usize = 8;
79
80/// 可选元素(安排可能为 未选取 的元素)的最大数量,超过这个数量会被拒绝
81/// 这个限制来自位图的大小:16 * 64 = 1024 位
82pub const 最大元素数量: usize = 1024;
83
84/// 可编码对象的序列
85pub type 元素序列 = [元素位; 最大元素序列长度];
86
87/// 元素关系图
88pub type 元素图 = FxHashMap<元素, Vec<元素>>;
89
90/// 最大元素编码长度
91pub const 最大元素编码长度: usize = 4;
92
93/// 编码用无符号整数表示
94pub type 编码 = u64;
95
96/// 包含词、词长、元素序列、频率等信息
97#[derive(Debug, Clone)]
98pub struct 可编码对象 {
99    pub 词: String,
100    pub 词长: usize,
101    pub 元素序列: 元素序列,
102    pub 全部元素序列: Vec<(元素序列, 位图)>,
103    pub 频率: u64,
104    pub 简码长度: u64,
105    pub 原始顺序: usize,
106}
107
108// 位图,用于表示元素是否存在,最多支持 1024 个元素
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct 位图 {
111    pub 位图: [u64; 16],
112}
113
114impl 位图 {
115    pub fn new() -> Self {
116        Self { 位图: [0; 16] }
117    }
118
119    pub fn 从条件列表创建(条件列表: &[条件], 棱镜: &棱镜) -> Self {
120        let mut bitmap = Self::new();
121        for 条件 in 条件列表 {
122            if let Some(&元素) = 棱镜.元素转数字.get(&条件.element) {
123                if let Some(&位图索引) = 棱镜.可选元素位图索引.get(&元素) {
124                    bitmap.insert(位图索引);
125                } else {
126                    // 如果条件中的元素不在棱镜中,说明配置文件有问题,直接忽略这个条件
127                    // 也可以选择抛出错误,但考虑到健壮性,这里选择忽略
128                    eprintln!(
129                        "警告:条件中的元素「{}」在棱镜中未找到,已忽略这个条件",
130                        条件.element
131                    );
132                }
133            }
134        }
135        bitmap
136    }
137
138    pub fn insert(&mut self, i: usize) {
139        let (block, bit) = (i / 64, i % 64);
140        self.位图[block] |= 1 << bit;
141    }
142
143    pub fn remove(&mut self, i: usize) {
144        let (block, bit) = (i / 64, i % 64);
145        self.位图[block] &= !(1 << bit);
146    }
147
148    pub fn subset(&self, other: &Self) -> bool {
149        for i in 0..16 {
150            if self.位图[i] & !other.位图[i] != 0 {
151                return false;
152            }
153        }
154        true
155    }
156
157    pub fn union(&self, other: &Self) -> Self {
158        let mut result = Self::new();
159        for i in 0..16 {
160            result.位图[i] = self.位图[i] | other.位图[i];
161        }
162        result
163    }
164
165    pub fn intersection(&self, other: &Self) -> Self {
166        let mut result = Self::new();
167        for i in 0..16 {
168            result.位图[i] = self.位图[i] & other.位图[i];
169        }
170        result
171    }
172}
173
174/// 全码或简码的编码信息
175#[derive(Clone, Debug, Copy, Default)]
176pub struct 部分编码信息 {
177    pub 原始编码: 编码,       // 原始编码
178    pub 原始编码候选位置: u8, // 原始编码上的选重位置
179    pub 实际编码: 编码,       // 实际编码
180    pub 选重标记: u8,         // 实际编码的候选位置(0 表示首选,>0 表示选重)
181    pub 上一个实际编码: 编码, // 前一个实际编码
182    pub 上一个选重标记: u8,   // 前一个实际编码的候选位置
183    pub 有变化: bool,         // 编码是否发生了变化
184}
185
186impl 部分编码信息 {
187    #[inline(always)]
188    pub fn 更新(&mut self, 编码: 编码, 选重标记: u8) {
189        if self.实际编码 == 编码 && self.选重标记 == 选重标记 {
190            return;
191        }
192        self.有变化 = true;
193        self.上一个实际编码 = self.实际编码;
194        self.上一个选重标记 = self.选重标记;
195        self.实际编码 = 编码;
196        self.选重标记 = 选重标记;
197    }
198}
199
200/// 包含长度、频率、全码和简码,用于传给目标函数来统计
201#[derive(Clone, Debug)]
202pub struct 编码信息 {
203    pub 词长: usize,
204    pub 频率: u64,
205    pub 全码: 部分编码信息,
206    pub 简码: 部分编码信息,
207}
208
209impl 编码信息 {
210    pub fn new(词: &可编码对象) -> Self {
211        Self {
212            词长: 词.词长,
213            频率: 词.频率,
214            全码: 部分编码信息::default(),
215            简码: 部分编码信息::default(),
216        }
217    }
218}
219
220/// 按键用无符号整数表示
221pub type 键 = u64;
222
223/// 用指标记
224pub type 指法向量 = [u8; 8];
225
226/// 自动上屏判断数组
227pub type 自动上屏 = Vec<bool>;
228
229/// 用于输出为文本码表,包含了名称、全码、简码、全码排名和简码排名
230#[derive(Debug, Clone, Serialize, Default)]
231pub struct 码表项 {
232    pub 词: String,
233    pub 全码: String,
234    pub 全码排名: u8,
235    pub 简码: String,
236    pub 简码排名: u8,
237}
238
239impl 安排 {
240    pub fn normalize(&self) -> Vec<广义码位> {
241        match self {
242            安排::Advanced(vector) => vector.clone(),
243            安排::Basic(string) => string.chars().map(广义码位::Ascii).collect(),
244            _ => panic!("无法把归并或禁用表示成列表形式"),
245        }
246    }
247}
248
249pub fn 元素标准名称(element: &String, index: usize) -> String {
250    if index == 0 {
251        element.to_string()
252    } else {
253        format!("{element}.{index}")
254    }
255}
256
257#[derive(Debug, Clone)]
258pub struct 棱镜 {
259    pub 键转数字: FxHashMap<char, 键>,
260    pub 数字转键: FxHashMap<键, char>,
261    pub 元素转数字: FxHashMap<String, 元素>,
262    pub 数字转元素: FxHashMap<元素, String>,
263    pub 进制: u64,
264    /// 安排可能为 未选取 的元素的紧凑位图索引,仅包含这些元素
265    /// 键为棱镜中的元素编号,值为位图中的紧凑索引(0, 1, 2, ...)
266    pub 可选元素位图索引: FxHashMap<元素, usize>,
267}
268
269impl 棱镜 {
270    pub fn 元素总数(&self) -> usize {
271        self.元素转数字.len() + 1 // 加上 0 这个特殊元素
272    }
273
274    /// 如前所述,建立了一个按键到整数的映射之后,可以将字符串看成具有某个进制的数。所以,给定一个数,也可以把它转化为字符串
275    pub fn 数字转编码(&self, code: 编码) -> Vec<char> {
276        let mut chars = Vec::new();
277        let mut remainder = code;
278        while remainder > 0 {
279            let k = remainder % self.进制;
280            remainder /= self.进制;
281            if k == 0 {
282                continue;
283            }
284            let char = self.数字转键.get(&k).unwrap(); // 从内部表示转换为字符,不需要检查
285            chars.push(*char);
286        }
287        chars
288    }
289
290    pub fn 预处理元素序列(
291        &self,
292        词: &String,
293        原始元素序列: &[广义码位],
294        最大码长: usize,
295    ) -> Result<元素序列, 错误> {
296        let mut 元素序列 = 元素序列::default();
297        let 原始元素序列长度 = 原始元素序列.len();
298        if 原始元素序列长度 > 最大码长 {
299            return Err(format!(
300                "编码对象「{词}」包含的元素数量为 {原始元素序列长度},超过了最大码长 {最大码长}"
301            )
302            .into());
303        }
304        for (i, 码位) in 原始元素序列.iter().enumerate() {
305            let 元素位 = match 码位 {
306                广义码位::Reference { element, index } => {
307                    if let Some(&元素) = self.元素转数字.get(element) {
308                        元素 + index * self.元素总数()
309                    } else {
310                        return Err(format!(
311                            "编码对象「{词}」包含的元素「{element}」无法在键盘映射中找到"
312                        )
313                        .into());
314                    }
315                }
316                广义码位::Ascii(k) => {
317                    if let Some(&键) = self.键转数字.get(k) {
318                        // 把按键转换为元素位,按键 0 对应元素位 0,按键 1 对应元素位 1,以此类推
319                        as usize
320                    } else {
321                        return Err(format!(
322                            "编码对象「{词}」包含的按键「{k}」无法在键盘映射中找到"
323                        )
324                        .into());
325                    }
326                }
327                _ => 0,
328            };
329            元素序列[i] = 元素位;
330        }
331        return Ok(元素序列);
332    }
333
334    pub fn 预处理词列表(
335        &self,
336        原始词列表: Vec<原始可编码对象>,
337        最大码长: usize,
338    ) -> Result<Vec<可编码对象>, 错误> {
339        let mut 词列表 = vec![];
340        for (原始顺序, 原始可编码对象) in 原始词列表.into_iter().enumerate() {
341            let 原始可编码对象 {
342                词,
343                频率,
344                元素序列: 原始单一元素序列,
345                全部元素序列: 原始全部元素序列,
346                简码长度,
347            } = 原始可编码对象;
348            let 原始全部元素序列: Vec<原始元素序列及条件列表> = match (原始单一元素序列, 原始全部元素序列) {
349                (Some(seq), None) => vec![原始元素序列及条件列表 { 元素序列: seq, 条件列表: vec![] }],
350                (None, Some(list)) => list,
351                _ => panic!("编码对象「{词}」必须恰好提供「元素序列」或「全部元素序列」之一", 词 = 词),
352            };
353            let mut 全部元素序列 = vec![];
354            assert!(!原始全部元素序列.is_empty(), "编码对象「{词}」至少需要一个元素序列", 词 = 词);
355            assert!(原始全部元素序列.last().unwrap().条件列表.is_empty(), "编码对象「{词}」的最后一个元素序列必须没有任何条件", 词 = 词);
356            for 原始元素序列及条件列表 { 元素序列: 原始元素序列, 条件列表 } in 原始全部元素序列 {
357                let 元素序列 = self.预处理元素序列(&词, &原始元素序列, 最大码长)?;
358                let 位图 = 位图::从条件列表创建(&条件列表, self);
359                全部元素序列.push((元素序列, 位图));
360            }
361            let c = 可编码对象 {
362                词: 词.clone(),
363                词长: 词.chars().count(),
364                频率,
365                简码长度,
366                元素序列: 全部元素序列[0].0,
367                全部元素序列,
368                原始顺序,
369            };
370            词列表.push(c);
371        }
372        词列表.sort_by_key(|x| Reverse(x.频率));
373        Ok(词列表)
374    }
375
376    /// 根据编码字符和未归一化的键位分布,生成一个理想的键位分布
377    pub fn 预处理键位分布信息(
378        &self,
379        原始键位分布信息: &原始键位分布信息,
380    ) -> Vec<键位分布损失函数> {
381        let default_loss = 键位分布损失函数 {
382            理想值: 0.0,
383            低于惩罚: 0.0,
384            高于惩罚: 0.0,
385        };
386        let mut 键位分布信息: Vec<键位分布损失函数> = (0..self.进制)
387            .map(|键| {
388                // 0 只是为了占位,不需要统计
389                if 键 == 0 {
390                    default_loss.clone()
391                } else {
392                    let 键名称 = self.数字转键[&键];
393                    原始键位分布信息
394                        .get(&键名称)
395                        .unwrap_or(&default_loss)
396                        .clone()
397                }
398            })
399            .collect();
400        键位分布信息.iter_mut().for_each(|x| {
401            x.理想值 /= 100.0;
402        });
403        键位分布信息
404    }
405
406    /// 将编码空间内所有的编码组合预先计算好速度当量
407    /// 按照这个字符串所对应的整数为下标,存储到一个大数组中
408    pub fn 预处理当量信息(
409        &self, 原始当量信息: &原始当量信息, space: usize
410    ) -> Vec<f64> {
411        let mut result: Vec<f64> = vec![0.0; space];
412        for (index, equivalence) in result.iter_mut().enumerate() {
413            let chars = self.数字转编码(index as u64);
414            for correlation_length in [2, 3, 4] {
415                if chars.len() < correlation_length {
416                    break;
417                }
418                // N 键当量
419                for i in 0..=(chars.len() - correlation_length) {
420                    let substr: String = chars[i..(i + correlation_length)].iter().collect();
421                    *equivalence += 原始当量信息.get(&substr).unwrap_or(&0.0);
422                }
423            }
424        }
425        result
426    }
427
428    /// 将编码空间内所有的编码组合预先计算好差指法标记
429    /// 标记压缩到一个 64 位整数中,每四位表示一个字符的差指法标记
430    /// 从低位到高位,依次是:同手、同指大跨排、同指小跨排、小指干扰、错手、三连击
431    /// 按照这个字符串所对应的整数为下标,存储到一个大数组中
432    pub fn 预处理指法标记(&self, 空间: usize) -> Vec<指法向量> {
433        let 指法标记 = 指法标记::new();
434        let mut result: Vec<指法向量> = Vec::with_capacity(空间);
435        for code in 0..空间 {
436            let chars = self.数字转编码(code as u64);
437            if chars.len() < 2 {
438                result.push(指法向量::default());
439                continue;
440            }
441            let mut 指法向量 = 指法向量::default();
442            for i in 0..(chars.len() - 1) {
443                let pair = (chars[i], chars[i + 1]);
444                if 指法标记.同手.contains(&pair) {
445                    指法向量[0] += 1;
446                }
447                if 指法标记.同指大跨排.contains(&pair) {
448                    指法向量[1] += 1;
449                }
450                if 指法标记.同指小跨排.contains(&pair) {
451                    指法向量[2] += 1;
452                }
453                if 指法标记.小指干扰.contains(&pair) {
454                    指法向量[3] += 1;
455                }
456                if 指法标记.错手.contains(&pair) {
457                    指法向量[4] += 1;
458                }
459            }
460            for i in 0..(chars.len() - 2) {
461                let triple = (chars[i], chars[i + 1], chars[i + 2]);
462                if triple.0 == triple.1 && triple.1 == triple.2 {
463                    指法向量[5] += 1;
464                }
465            }
466            result.push(指法向量);
467        }
468        result
469    }
470}
471
472/// 错误类型
473#[derive(Debug, Clone)]
474pub struct 错误 {
475    pub message: String,
476}
477
478impl From<String> for 错误 {
479    fn from(value: String) -> Self {
480        Self { message: value }
481    }
482}
483
484impl From<&str> for 错误 {
485    fn from(value: &str) -> Self {
486        Self {
487            message: value.to_string(),
488        }
489    }
490}
491
492impl From<io::Error> for 错误 {
493    fn from(value: io::Error) -> Self {
494        Self {
495            message: value.to_string(),
496        }
497    }
498}
499
500impl From<serde_json::Error> for 错误 {
501    fn from(value: serde_json::Error) -> Self {
502        Self {
503            message: value.to_string(),
504        }
505    }
506}
507
508impl From<错误> for JsError {
509    fn from(value: 错误) -> Self {
510        JsError::new(&value.message)
511    }
512}
513
514#[cfg(target_arch = "wasm32")]
515pub fn formatted_local_now() -> String {
516    use js_sys::Date;
517    let date = Date::new_0();
518    let month = date.get_month() as u32 + 1; // JS 月份从 0 开始
519    let day = date.get_date() as u32;
520    let hour = date.get_hours() as u32;
521    let minute = date.get_minutes() as u32;
522    let second = date.get_seconds() as u32;
523    format!(
524        "{:02}-{:02}+{:02}_{:02}_{:02}",
525        month, day, hour, minute, second
526    )
527}
528
529#[cfg(not(target_arch = "wasm32"))]
530pub fn formatted_local_now() -> String {
531    use chrono::Local;
532    Local::now().format("%m-%d+%H_%M_%S").to_string()
533}