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                _ => 0,
317            };
318            元素序列[i] = 元素位;
319        }
320        return Ok(元素序列);
321    }
322
323    pub fn 预处理词列表(
324        &self,
325        原始词列表: Vec<原始可编码对象>,
326        最大码长: usize,
327    ) -> Result<Vec<可编码对象>, 错误> {
328        let mut 词列表 = vec![];
329        for (原始顺序, 原始可编码对象) in 原始词列表.into_iter().enumerate() {
330            let 原始可编码对象 {
331                词,
332                频率,
333                元素序列: 原始单一元素序列,
334                全部元素序列: 原始全部元素序列,
335                简码长度,
336            } = 原始可编码对象;
337            let 原始全部元素序列: Vec<原始元素序列及条件列表> = match (原始单一元素序列, 原始全部元素序列) {
338                (Some(seq), None) => vec![原始元素序列及条件列表 { 元素序列: seq, 条件列表: vec![] }],
339                (None, Some(list)) => list,
340                _ => panic!("编码对象「{词}」必须恰好提供「元素序列」或「全部元素序列」之一", 词 = 词),
341            };
342            let mut 全部元素序列 = vec![];
343            assert!(!原始全部元素序列.is_empty(), "编码对象「{词}」至少需要一个元素序列", 词 = 词);
344            assert!(原始全部元素序列.last().unwrap().条件列表.is_empty(), "编码对象「{词}」的最后一个元素序列必须没有任何条件", 词 = 词);
345            for 原始元素序列及条件列表 { 元素序列: 原始元素序列, 条件列表 } in 原始全部元素序列 {
346                let 元素序列 = self.预处理元素序列(&词, &原始元素序列, 最大码长)?;
347                let 位图 = 位图::从条件列表创建(&条件列表, self);
348                全部元素序列.push((元素序列, 位图));
349            }
350            let c = 可编码对象 {
351                词: 词.clone(),
352                词长: 词.chars().count(),
353                频率,
354                简码长度,
355                元素序列: 全部元素序列[0].0,
356                全部元素序列,
357                原始顺序,
358            };
359            词列表.push(c);
360        }
361        词列表.sort_by_key(|x| Reverse(x.频率));
362        Ok(词列表)
363    }
364
365    /// 根据编码字符和未归一化的键位分布,生成一个理想的键位分布
366    pub fn 预处理键位分布信息(
367        &self,
368        原始键位分布信息: &原始键位分布信息,
369    ) -> Vec<键位分布损失函数> {
370        let default_loss = 键位分布损失函数 {
371            理想值: 0.0,
372            低于惩罚: 0.0,
373            高于惩罚: 0.0,
374        };
375        let mut 键位分布信息: Vec<键位分布损失函数> = (0..self.进制)
376            .map(|键| {
377                // 0 只是为了占位,不需要统计
378                if 键 == 0 {
379                    default_loss.clone()
380                } else {
381                    let 键名称 = self.数字转键[&键];
382                    原始键位分布信息
383                        .get(&键名称)
384                        .unwrap_or(&default_loss)
385                        .clone()
386                }
387            })
388            .collect();
389        键位分布信息.iter_mut().for_each(|x| {
390            x.理想值 /= 100.0;
391        });
392        键位分布信息
393    }
394
395    /// 将编码空间内所有的编码组合预先计算好速度当量
396    /// 按照这个字符串所对应的整数为下标,存储到一个大数组中
397    pub fn 预处理当量信息(
398        &self, 原始当量信息: &原始当量信息, space: usize
399    ) -> Vec<f64> {
400        let mut result: Vec<f64> = vec![0.0; space];
401        for (index, equivalence) in result.iter_mut().enumerate() {
402            let chars = self.数字转编码(index as u64);
403            for correlation_length in [2, 3, 4] {
404                if chars.len() < correlation_length {
405                    break;
406                }
407                // N 键当量
408                for i in 0..=(chars.len() - correlation_length) {
409                    let substr: String = chars[i..(i + correlation_length)].iter().collect();
410                    *equivalence += 原始当量信息.get(&substr).unwrap_or(&0.0);
411                }
412            }
413        }
414        result
415    }
416
417    /// 将编码空间内所有的编码组合预先计算好差指法标记
418    /// 标记压缩到一个 64 位整数中,每四位表示一个字符的差指法标记
419    /// 从低位到高位,依次是:同手、同指大跨排、同指小跨排、小指干扰、错手、三连击
420    /// 按照这个字符串所对应的整数为下标,存储到一个大数组中
421    pub fn 预处理指法标记(&self, 空间: usize) -> Vec<指法向量> {
422        let 指法标记 = 指法标记::new();
423        let mut result: Vec<指法向量> = Vec::with_capacity(空间);
424        for code in 0..空间 {
425            let chars = self.数字转编码(code as u64);
426            if chars.len() < 2 {
427                result.push(指法向量::default());
428                continue;
429            }
430            let mut 指法向量 = 指法向量::default();
431            for i in 0..(chars.len() - 1) {
432                let pair = (chars[i], chars[i + 1]);
433                if 指法标记.同手.contains(&pair) {
434                    指法向量[0] += 1;
435                }
436                if 指法标记.同指大跨排.contains(&pair) {
437                    指法向量[1] += 1;
438                }
439                if 指法标记.同指小跨排.contains(&pair) {
440                    指法向量[2] += 1;
441                }
442                if 指法标记.小指干扰.contains(&pair) {
443                    指法向量[3] += 1;
444                }
445                if 指法标记.错手.contains(&pair) {
446                    指法向量[4] += 1;
447                }
448            }
449            for i in 0..(chars.len() - 2) {
450                let triple = (chars[i], chars[i + 1], chars[i + 2]);
451                if triple.0 == triple.1 && triple.1 == triple.2 {
452                    指法向量[5] += 1;
453                }
454            }
455            result.push(指法向量);
456        }
457        result
458    }
459}
460
461/// 错误类型
462#[derive(Debug, Clone)]
463pub struct 错误 {
464    pub message: String,
465}
466
467impl From<String> for 错误 {
468    fn from(value: String) -> Self {
469        Self { message: value }
470    }
471}
472
473impl From<&str> for 错误 {
474    fn from(value: &str) -> Self {
475        Self {
476            message: value.to_string(),
477        }
478    }
479}
480
481impl From<io::Error> for 错误 {
482    fn from(value: io::Error) -> Self {
483        Self {
484            message: value.to_string(),
485        }
486    }
487}
488
489impl From<serde_json::Error> for 错误 {
490    fn from(value: serde_json::Error) -> Self {
491        Self {
492            message: value.to_string(),
493        }
494    }
495}
496
497impl From<错误> for JsError {
498    fn from(value: 错误) -> Self {
499        JsError::new(&value.message)
500    }
501}
502
503#[cfg(target_arch = "wasm32")]
504pub fn formatted_local_now() -> String {
505    use js_sys::Date;
506    let date = Date::new_0();
507    let month = date.get_month() as u32 + 1; // JS 月份从 0 开始
508    let day = date.get_date() as u32;
509    let hour = date.get_hours() as u32;
510    let minute = date.get_minutes() as u32;
511    let second = date.get_seconds() as u32;
512    format!(
513        "{:02}-{:02}+{:02}_{:02}_{:02}",
514        month, day, hour, minute, second
515    )
516}
517
518#[cfg(not(target_arch = "wasm32"))]
519pub fn formatted_local_now() -> String {
520    use chrono::Local;
521    Local::now().format("%m-%d+%H_%M_%S").to_string()
522}