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;
12pub mod server;
13
14use config::{安排, 广义码位};
15use objectives::metric::指法标记;
16use rustc_hash::FxHashMap;
17use serde::{Deserialize, Serialize};
18use std::cmp::Reverse;
19use std::io;
20use wasm_bindgen::JsError;
21
22/// 只考虑长度为 1 到 10 的词
23pub const 最大词长: usize = 10;
24
25/// 只对低于最大按键组合长度的编码预先计算当量
26pub const 最大按键组合长度: usize = 4;
27
28/// 从配置文件中读取的原始可编码对象
29#[derive(Debug, Serialize, Deserialize, Clone)]
30pub struct 原始可编码对象 {
31    pub 词: String,
32    pub 元素序列: String,
33    pub 频率: u64,
34    #[serde(default = "原始可编码对象::默认级别")]
35    pub 简码长度: u64,
36}
37
38impl 原始可编码对象 {
39    const fn 默认级别() -> u64 {
40        u64::MAX
41    }
42}
43
44pub type 原始键位分布信息 = FxHashMap<char, 键位分布损失函数>;
45pub type 键位分布信息 = Vec<键位分布损失函数>;
46pub type 原始当量信息 = FxHashMap<String, f64>;
47pub type 当量信息 = Vec<f64>;
48
49/// 键位分布的理想值和惩罚值
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct 键位分布损失函数 {
52    pub 理想值: f64,
53    pub 低于惩罚: f64,
54    pub 高于惩罚: f64,
55}
56
57/// 元素用一个无符号整数表示
58pub type 元素 = usize;
59
60/// 可编码对象的序列
61pub type 元素序列 = Vec<(元素, usize)>;
62
63/// 元素关系图
64pub type 元素图 = FxHashMap<元素, Vec<元素>>;
65
66/// 最大元素编码长度
67pub const 最大元素编码长度: usize = 4;
68
69/// 编码用无符号整数表示
70pub type 编码 = u64;
71
72/// 包含词、词长、元素序列、频率等信息
73#[derive(Debug, Clone)]
74pub struct 可编码对象 {
75    pub 词: String,
76    pub 词长: usize,
77    pub 元素序列: 元素序列,
78    pub 频率: u64,
79    pub 简码长度: u64,
80    pub 原始顺序: usize,
81}
82
83/// 全码或简码的编码信息
84#[derive(Clone, Debug, Copy, Default)]
85pub struct 部分编码信息 {
86    pub 原始编码: 编码,       // 原始编码
87    pub 原始编码候选位置: u8, // 原始编码上的选重位置
88    pub 实际编码: 编码,       // 实际编码
89    pub 选重标记: bool,       // 实际编码是否算作重码
90    pub 上一个实际编码: 编码, // 前一个实际编码
91    pub 上一个选重标记: bool, // 前一个实际编码是否算作重码
92    pub 有变化: bool,         // 编码是否发生了变化
93}
94
95impl 部分编码信息 {
96    #[inline(always)]
97    pub fn 更新(&mut self, 编码: 编码, 选重标记: bool) {
98        if self.实际编码 == 编码 && self.选重标记 == 选重标记 {
99            return;
100        }
101        self.有变化 = true;
102        self.上一个实际编码 = self.实际编码;
103        self.上一个选重标记 = self.选重标记;
104        self.实际编码 = 编码;
105        self.选重标记 = 选重标记;
106    }
107}
108
109/// 包含长度、频率、全码和简码,用于传给目标函数来统计
110#[derive(Clone, Debug)]
111pub struct 编码信息 {
112    pub 词长: usize,
113    pub 频率: u64,
114    pub 全码: 部分编码信息,
115    pub 简码: 部分编码信息,
116}
117
118impl 编码信息 {
119    pub fn new(词: &可编码对象) -> Self {
120        Self {
121            词长: 词.词长,
122            频率: 词.频率,
123            全码: 部分编码信息::default(),
124            简码: 部分编码信息::default(),
125        }
126    }
127}
128
129/// 按键用无符号整数表示
130pub type 键 = u64;
131
132/// 用指标记
133pub type 指法向量 = [u8; 8];
134
135/// 自动上屏判断数组
136pub type 自动上屏 = Vec<bool>;
137
138/// 用于输出为文本码表,包含了名称、全码、简码、全码排名和简码排名
139#[derive(Debug, Clone, Serialize, Default)]
140pub struct 码表项 {
141    pub 词: String,
142    pub 全码: String,
143    pub 全码排名: u8,
144    pub 简码: String,
145    pub 简码排名: u8,
146}
147
148impl 安排 {
149    pub fn normalize(&self) -> Vec<广义码位> {
150        match self {
151            安排::Advanced(vector) => vector.clone(),
152            安排::Basic(string) => string.chars().map(广义码位::Ascii).collect(),
153            _ => panic!("无法把归并或禁用表示成列表形式"),
154        }
155    }
156}
157
158pub fn 元素标准名称(element: &String, index: usize) -> String {
159    if index == 0 {
160        element.to_string()
161    } else {
162        format!("{element}.{index}")
163    }
164}
165
166#[derive(Debug, Clone)]
167pub struct 棱镜 {
168    pub 键转数字: FxHashMap<char, 键>,
169    pub 数字转键: FxHashMap<键, char>,
170    pub 元素转数字: FxHashMap<String, 元素>,
171    pub 数字转元素: FxHashMap<元素, String>,
172    pub 进制: u64,
173}
174
175impl 棱镜 {
176    /// 如前所述,建立了一个按键到整数的映射之后,可以将字符串看成具有某个进制的数。所以,给定一个数,也可以把它转化为字符串
177    pub fn 数字转编码(&self, code: 编码) -> Vec<char> {
178        let mut chars = Vec::new();
179        let mut remainder = code;
180        while remainder > 0 {
181            let k = remainder % self.进制;
182            remainder /= self.进制;
183            if k == 0 {
184                continue;
185            }
186            let char = self.数字转键.get(&k).unwrap(); // 从内部表示转换为字符,不需要检查
187            chars.push(*char);
188        }
189        chars
190    }
191
192    pub fn 预处理词列表(
193        &self,
194        原始词列表: Vec<原始可编码对象>,
195        最大码长: usize,
196    ) -> Result<Vec<可编码对象>, 错误> {
197        let mut 词列表 = Vec::new();
198        for (原始顺序, 原始可编码对象) in 原始词列表.into_iter().enumerate() {
199            let 原始可编码对象 {
200                词: name,
201                频率: frequency,
202                元素序列: sequence,
203                简码长度: level,
204            } = 原始可编码对象;
205            let 原始元素序列: Vec<_> = sequence.split(' ').collect();
206            let mut 元素序列 = 元素序列::new();
207            let length = 原始元素序列.len();
208            if length > 最大码长 {
209                return Err(format!(
210                    "编码对象「{name}」包含的元素数量为 {length},超过了最大码长 {最大码长}"
211                )
212                .into());
213            }
214            for 原始元素 in 原始元素序列 {
215                let (元素, 位置) = if 原始元素.contains(".") {
216                    let parts: Vec<&str> = 原始元素.split('.').collect();
217                    if parts.len() != 2 {
218                        return Err(format!(
219                            "编码对象「{name}」包含的元素「{原始元素}」格式不正确"
220                        )
221                        .into());
222                    }
223                    let 元素名称 = parts[0];
224                    let index: usize = match parts[1].parse() {
225                        Ok(v) => v,
226                        Err(_) => {
227                            return Err(format!(
228                                "编码对象「{name}」包含的元素「{原始元素}」格式不正确"
229                            )
230                            .into());
231                        }
232                    };
233                    if let Some(元素) = self.元素转数字.get(元素名称) {
234                        元素序列.push((*元素, index));
235                    } else {
236                        return Err(format!(
237                            "编码对象「{name}」包含的元素「{原始元素}」无法在键盘映射中找到"
238                        )
239                        .into());
240                    }
241                    continue;
242                } else {
243                    let 元素名称 = 原始元素;
244                    if let Some(元素) = self.元素转数字.get(元素名称) {
245                        (*元素, 0)
246                    } else {
247                        return Err(format!(
248                            "编码对象「{name}」包含的元素「{原始元素}」无法在键盘映射中找到"
249                        )
250                        .into());
251                    }
252                };
253                元素序列.push((元素, 位置));
254            }
255            词列表.push(可编码对象 {
256                词: name.clone(),
257                词长: name.chars().count(),
258                频率: frequency,
259                简码长度: level,
260                元素序列,
261                原始顺序,
262            });
263        }
264        词列表.sort_by_key(|x| Reverse(x.频率));
265        Ok(词列表)
266    }
267
268    /// 根据编码字符和未归一化的键位分布,生成一个理想的键位分布
269    pub fn 预处理键位分布信息(
270        &self,
271        原始键位分布信息: &原始键位分布信息,
272    ) -> Vec<键位分布损失函数> {
273        let default_loss = 键位分布损失函数 {
274            理想值: 0.0,
275            低于惩罚: 0.0,
276            高于惩罚: 0.0,
277        };
278        let mut 键位分布信息: Vec<键位分布损失函数> = (0..self.进制)
279            .map(|键| {
280                // 0 只是为了占位,不需要统计
281                if 键 == 0 {
282                    default_loss.clone()
283                } else {
284                    let 键名称 = self.数字转键[&键];
285                    原始键位分布信息
286                        .get(&键名称)
287                        .unwrap_or(&default_loss)
288                        .clone()
289                }
290            })
291            .collect();
292        键位分布信息.iter_mut().for_each(|x| {
293            x.理想值 /= 100.0;
294        });
295        键位分布信息
296    }
297
298    /// 将编码空间内所有的编码组合预先计算好速度当量
299    /// 按照这个字符串所对应的整数为下标,存储到一个大数组中
300    pub fn 预处理当量信息(
301        &self, 原始当量信息: &原始当量信息, space: usize
302    ) -> Vec<f64> {
303        let mut result: Vec<f64> = vec![0.0; space];
304        for (index, equivalence) in result.iter_mut().enumerate() {
305            let chars = self.数字转编码(index as u64);
306            for correlation_length in [2, 3, 4] {
307                if chars.len() < correlation_length {
308                    break;
309                }
310                // N 键当量
311                for i in 0..=(chars.len() - correlation_length) {
312                    let substr: String = chars[i..(i + correlation_length)].iter().collect();
313                    *equivalence += 原始当量信息.get(&substr).unwrap_or(&0.0);
314                }
315            }
316        }
317        result
318    }
319
320    /// 将编码空间内所有的编码组合预先计算好差指法标记
321    /// 标记压缩到一个 64 位整数中,每四位表示一个字符的差指法标记
322    /// 从低位到高位,依次是:同手、同指大跨排、同指小跨排、小指干扰、错手、三连击
323    /// 按照这个字符串所对应的整数为下标,存储到一个大数组中
324    pub fn 预处理指法标记(&self, 空间: usize) -> Vec<指法向量> {
325        let 指法标记 = 指法标记::new();
326        let mut result: Vec<指法向量> = Vec::with_capacity(空间);
327        for code in 0..空间 {
328            let chars = self.数字转编码(code as u64);
329            if chars.len() < 2 {
330                result.push(指法向量::default());
331                continue;
332            }
333            let mut 指法向量 = 指法向量::default();
334            for i in 0..(chars.len() - 1) {
335                let pair = (chars[i], chars[i + 1]);
336                if 指法标记.同手.contains(&pair) {
337                    指法向量[0] += 1;
338                }
339                if 指法标记.同指大跨排.contains(&pair) {
340                    指法向量[1] += 1;
341                }
342                if 指法标记.同指小跨排.contains(&pair) {
343                    指法向量[2] += 1;
344                }
345                if 指法标记.小指干扰.contains(&pair) {
346                    指法向量[3] += 1;
347                }
348                if 指法标记.错手.contains(&pair) {
349                    指法向量[4] += 1;
350                }
351            }
352            for i in 0..(chars.len() - 2) {
353                let triple = (chars[i], chars[i + 1], chars[i + 2]);
354                if triple.0 == triple.1 && triple.1 == triple.2 {
355                    指法向量[5] += 1;
356                }
357            }
358            result.push(指法向量);
359        }
360        result
361    }
362}
363
364/// 错误类型
365#[derive(Debug, Clone)]
366pub struct 错误 {
367    pub message: String,
368}
369
370impl From<String> for 错误 {
371    fn from(value: String) -> Self {
372        Self { message: value }
373    }
374}
375
376impl From<&str> for 错误 {
377    fn from(value: &str) -> Self {
378        Self {
379            message: value.to_string(),
380        }
381    }
382}
383
384impl From<io::Error> for 错误 {
385    fn from(value: io::Error) -> Self {
386        Self {
387            message: value.to_string(),
388        }
389    }
390}
391
392impl From<serde_json::Error> for 错误 {
393    fn from(value: serde_json::Error) -> Self {
394        Self {
395            message: value.to_string(),
396        }
397    }
398}
399
400impl From<错误> for JsError {
401    fn from(value: 错误) -> Self {
402        JsError::new(&value.message)
403    }
404}