msime-engine 0.2.0

Input method engine for Chinese and other languages: quanpin, shuangpin, nine-key, wubi, zhuyin, Cantonese, Japanese, Korean and more, ranked over a word lattice with typo correction and learning from what the user picks. The engine of 水杉输入法 (Metasequoia IME).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
//! `V` 模式单位换算:数值后接源单位及可选的目标单位(`3jin'g`、`3jintog`、`5kmmi`、`-10cf`、`1mu`)。单位字母必须完整匹配拼音和缩写白名单;传给 rink-core 的查询由表内单位名称与经过校验的带符号十进制数值拼成。其余输入交给算式路径。
//!
//! rink-core is used without its bundled units database: the context starts empty and loads only the definitions written in this file, the SI and international units the table names (`BASE_DEFINITIONS`) and the 市制 units (`SHIZHI_DEFINITIONS`), each at its statutory value. Temperature is computed directly, because its scales differ by an offset as well as a factor and a plain ratio of units would be wrong.
//!
//! The context is built once and kept in a `OnceLock`, and `warm_up` (spawned in the background when the `V` mode is enabled) builds it ahead of the first conversion so that no key waits for it.

use std::sync::{Once, OnceLock};

use rink_core::output::QueryReply;
use rink_core::parsing::text_query;
use rink_core::Context;

use super::command::TEXT_UTF16_LIMIT;
use super::expression::{format_number, RESULT_LIMIT};

/// Every non-市制 unit `UNITS` names, in rink's definition syntax, with the kilogram and the metre as base units. The values are the SI definitions and the exact international values of 1959 (yard 0.9144 m, pound 0.45359237 kg); the acre is the international acre and the gallon the US liquid gallon of 231 cubic inches.
const BASE_DEFINITIONS: &str = "\
kg !
m !
g 1|1000 kg
mg 1|1000 g
tonne 1000 kg
lb 0.45359237 kg
oz 1|16 lb
km 1000 m
cm 1|100 m
mm 1|1000 m
inch 2.54 cm
ft 12 inch
yard 3 ft
mile 1760 yard
hectare 10000 m^2
acre 4840 yard^2
liter 1|1000 m^3
mL 1|1000 liter
usgallon 231 inch^3
";

/// The 市制 units as defined since 1959 (国务院《关于统一我国计量制度的命令》), in rink's definition syntax.
const SHIZHI_DEFINITIONS: &str =
    "shijin 500 g\nshiliang 50 g\nshimu 10000|15 m^2\nshili 500 m\nshichi 1|3 m\nshicun 1|30 m\n";

/// 单位前数值部分的字节上限,包含小数点,不包含可选的前缀符号。
const NUMBER_LIMIT: usize = 16;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Quantity {
    Mass,
    Length,
    Area,
    Volume,
    Temperature,
}

/// One unit of the closed table: the spellings that name it, the rink expression it stands for, the label a row shows, and whether it is listed when no target is typed.
struct Unit {
    aliases: &'static [&'static str],
    rink: &'static str,
    label: &'static str,
    quantity: Quantity,
    default_target: bool,
}

const fn unit(
    aliases: &'static [&'static str],
    rink: &'static str,
    label: &'static str,
    quantity: Quantity,
    default_target: bool,
) -> Unit {
    Unit {
        aliases,
        rink,
        label,
        quantity,
        default_target,
    }
}

/// Every unit the mode converts. An alias names exactly one unit: `mi` is the mile, as in the abbreviation typed after a number (`5kmmi`), not pinyin 米, which is `m`.
const UNITS: &[Unit] = &[
    unit(&["jin", "shijin"], "shijin", "斤", Quantity::Mass, true),
    unit(
        &["liang", "shiliang"],
        "shiliang",
        "两",
        Quantity::Mass,
        false,
    ),
    unit(&["g", "ke"], "g", "克", Quantity::Mass, true),
    unit(
        &["kg", "gongjin", "qianke"],
        "kg",
        "千克",
        Quantity::Mass,
        true,
    ),
    unit(&["mg", "haoke"], "mg", "毫克", Quantity::Mass, false),
    unit(&["t", "dun"], "tonne", "吨", Quantity::Mass, false),
    unit(&["lb", "bang"], "lb", "磅", Quantity::Mass, true),
    unit(&["oz", "angsi"], "oz", "盎司", Quantity::Mass, false),
    unit(&["li", "shili"], "shili", "里", Quantity::Length, true),
    unit(&["chi", "shichi"], "shichi", "尺", Quantity::Length, true),
    unit(&["cun", "shicun"], "shicun", "寸", Quantity::Length, false),
    unit(&["m"], "m", "米", Quantity::Length, true),
    unit(
        &["km", "gongli", "qianmi"],
        "km",
        "千米",
        Quantity::Length,
        true,
    ),
    unit(&["cm", "limi"], "cm", "厘米", Quantity::Length, true),
    unit(&["mm", "haomi"], "mm", "毫米", Quantity::Length, false),
    unit(
        &["mi", "mile", "yingli"],
        "mile",
        "英里",
        Quantity::Length,
        true,
    ),
    unit(&["ft", "yingchi"], "ft", "英尺", Quantity::Length, true),
    unit(&["inch", "yingcun"], "inch", "英寸", Quantity::Length, true),
    unit(&["yd", "ma"], "yard", "码", Quantity::Length, false),
    unit(&["mu", "shimu"], "shimu", "亩", Quantity::Area, true),
    unit(
        &["sqm", "pingfangmi"],
        "m^2",
        "平方米",
        Quantity::Area,
        true,
    ),
    unit(
        &["sqkm", "pingfanggongli"],
        "km^2",
        "平方千米",
        Quantity::Area,
        false,
    ),
    unit(&["ha", "gongqing"], "hectare", "公顷", Quantity::Area, true),
    unit(&["acre", "yingmu"], "acre", "英亩", Quantity::Area, true),
    unit(&["l", "sheng"], "liter", "升", Quantity::Volume, true),
    unit(&["ml", "haosheng"], "mL", "毫升", Quantity::Volume, true),
    unit(
        &["gal", "jialun"],
        "usgallon",
        "加仑",
        Quantity::Volume,
        true,
    ),
    unit(&["c", "sheshidu"], "", "℃", Quantity::Temperature, true),
    unit(&["f", "huashidu"], "", "℉", Quantity::Temperature, true),
    unit(&["k", "kaierwen"], "", "K", Quantity::Temperature, true),
];

static CONTEXT: OnceLock<Option<Context>> = OnceLock::new();
static WARM_UP: Once = Once::new();

/// Build the conversion context if it is not built yet. Blocks while the definitions are parsed, so call it off the key path.
pub fn warm_up() {
    context();
}

/// Start `warm_up` on a background thread, once per process.
pub fn warm_up_in_background() {
    WARM_UP.call_once(|| {
        // A failed spawn leaves the first conversion to build the context itself, which is slower but still correct.
        let _ = std::thread::Builder::new()
            .name("msime-unit-context".to_owned())
            .spawn(warm_up);
    });
}

fn context() -> Option<&'static Context> {
    CONTEXT
        .get_or_init(|| {
            let mut context = Context::new();
            context.load_definitions(BASE_DEFINITIONS).ok()?;
            context.load_definitions(SHIZHI_DEFINITIONS).ok()?;
            Some(context)
        })
        .as_ref()
}

/// 将符合 `[+|-] NUMBER UNIT [to|'] [UNIT]` 的输入转换为候选,否则返回 `None` 以继续算式路径。每行不超过 `TEXT_UTF16_LIMIT` 个 UTF-16 单元,最多返回 `RESULT_LIMIT` 行。
pub fn query_units(code: &str) -> Option<Vec<String>> {
    let unsigned = code
        .strip_prefix('-')
        .or_else(|| code.strip_prefix('+'))
        .unwrap_or(code);
    let number_end = unsigned
        .bytes()
        .position(|byte| !(byte.is_ascii_digit() || byte == b'.'))?;
    let magnitude = &unsigned[..number_end];
    if magnitude.is_empty()
        || magnitude.len() > NUMBER_LIMIT
        || magnitude.starts_with('.')
        || magnitude.ends_with('.')
        || magnitude.matches('.').count() > 1
    {
        return None;
    }
    let (number, letters) = code.split_at(code.len() - unsigned.len() + number_end);
    let value: f64 = number.parse().ok()?;
    let (source, target) = parse_units(letters)?;
    let mut rows: Vec<String> = Vec::with_capacity(result_capacity(source, target));
    let mut push = |row: String| {
        if rows.len() < RESULT_LIMIT
            && row.encode_utf16().count() <= TEXT_UTF16_LIMIT
            && !rows.contains(&row)
        {
            rows.push(row);
        }
    };
    let explicit = target.is_some();
    let mut convert_to = |target: &Unit| {
        let Some(converted) = convert(number, value, source, target) else {
            return;
        };
        let Some(text) = format_number(converted) else {
            return;
        };
        push(format!("{text}{}", target.label));
        if explicit {
            push(text.clone());
            push(format!("{number}{}={text}{}", source.label, target.label));
        }
    };
    if let Some(target) = target {
        convert_to(target);
    } else {
        for target in UNITS.iter().filter(|unit| {
            unit.default_target && unit.quantity == source.quantity && !std::ptr::eq(*unit, source)
        }) {
            convert_to(target);
        }
    }
    (!rows.is_empty()).then_some(rows)
}

/// The source unit and the optional target: an apostrophe or a literal `to` may stand between them. A whole input naming one unit is read as that unit; otherwise the longest source that leaves a unit (or nothing) behind wins.
fn parse_units(letters: &str) -> Option<(&'static Unit, Option<&'static Unit>)> {
    if letters.is_empty()
        || !letters
            .bytes()
            .all(|byte| byte.is_ascii_lowercase() || byte == b'\'')
    {
        return None;
    }
    if let Some((source, target)) = letters.split_once('\'') {
        let source = lookup(source)?;
        let target = if target.is_empty() {
            None
        } else {
            Some(lookup(
                target
                    .strip_prefix("to")
                    .filter(|rest| lookup(rest).is_some())
                    .unwrap_or(target),
            )?)
        };
        return compatible(source, target);
    }
    if let Some(source) = lookup(letters) {
        return Some((source, None));
    }
    for split in (1..letters.len()).rev() {
        let (source, rest) = letters.split_at(split);
        let Some(source) = lookup(source) else {
            continue;
        };
        let target = lookup(rest).or_else(|| rest.strip_prefix("to").and_then(lookup));
        if let Some(target) = target {
            if let Some(pair) = compatible(source, Some(target)) {
                return Some(pair);
            }
        }
    }
    None
}

fn compatible(
    source: &'static Unit,
    target: Option<&'static Unit>,
) -> Option<(&'static Unit, Option<&'static Unit>)> {
    match target {
        Some(target) if target.quantity != source.quantity || std::ptr::eq(target, source) => None,
        _ => Some((source, target)),
    }
}

fn lookup(alias: &str) -> Option<&'static Unit> {
    UNITS.iter().find(|unit| unit.aliases.contains(&alias))
}

fn result_capacity(source: &Unit, target: Option<&Unit>) -> usize {
    let targets = target.map_or_else(
        || {
            UNITS
                .iter()
                .filter(|unit| {
                    unit.default_target
                        && unit.quantity == source.quantity
                        && !std::ptr::eq(*unit, source)
                })
                .count()
        },
        |_| 3,
    );
    targets.min(RESULT_LIMIT)
}

/// 把源单位的 `value` 转成目标单位。`number` 保留经过校验的数值和可选前缀符号,原样交给 Rink 以保持十进制精度。
fn convert(number: &str, value: f64, source: &Unit, target: &Unit) -> Option<f64> {
    if source.quantity == Quantity::Temperature {
        let kelvin = match source.label {
            "℃" => value + 273.15,
            "℉" => (value - 32.0) * 5.0 / 9.0 + 273.15,
            _ => value,
        };
        let converted = match target.label {
            "℃" => kelvin - 273.15,
            "℉" => (kelvin - 273.15) * 9.0 / 5.0 + 32.0,
            _ => kelvin,
        };
        return converted.is_finite().then_some(converted);
    }
    let context = context()?;
    // 查询只拼接表内单位名称和经过校验、最多带一个前缀符号的数值。
    let query = format!("({number} {}) / ({})", source.rink, target.rink);
    let mut tokens = text_query::TokenIterator::new(&query).peekable();
    let parsed = text_query::parse_query(&mut tokens);
    let QueryReply::Number(parts) = context.eval_query(&parsed).ok()? else {
        return None;
    };
    let number = parts.raw_value?;
    // A ratio of two units of one quantity has no dimension left; anything else means the table paired the wrong units.
    if !number.unit.is_empty() {
        return None;
    }
    let converted = number.value.to_f64();
    converted.is_finite().then_some(converted)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ime::personal_rerank::allocations;

    fn rows(code: &str) -> Vec<String> {
        query_units(code).unwrap_or_default()
    }

    #[test]
    fn a_unit_and_a_target_convert_with_the_equation() {
        assert_eq!(rows("3jin'g"), ["1500克", "1500", "3斤=1500克"]);
        assert_eq!(rows("3jintog"), ["1500克", "1500", "3斤=1500克"]);
        assert_eq!(rows("3jing"), ["1500克", "1500", "3斤=1500克"]);
        assert_eq!(rows("5kmmi")[0], "3.10685596118667英里");
        assert_eq!(rows("1mile'km")[0], "1.609344千米");
        assert_eq!(rows("2liang'g")[0], "100克");
        assert_eq!(rows("1chi'cm")[0], "33.3333333333333厘米");
        assert_eq!(rows("1li'm")[0], "500米");
        assert_eq!(rows("1kg'lb")[0], "2.20462262184878磅");
    }

    #[test]
    fn temperatures_convert_with_their_offsets() {
        assert_eq!(rows("100cf"), ["212℉", "212", "100℃=212℉"]);
        assert_eq!(rows("32f'c")[0], "0℃");
        assert_eq!(rows("0c'k")[0], "273.15K");
    }

    #[test]
    fn signed_unit_measurements_are_converted() {
        assert_eq!(rows("-10cf"), ["14℉", "14", "-10℃=14℉"]);
        assert_eq!(rows("-40f'c"), ["-40℃", "-40", "-40℉=-40℃"]);
        assert_eq!(rows("+10cf"), ["50℉", "50", "+10℃=50℉"]);
        assert_eq!(rows("-0.5kg'g"), ["-500克", "-500", "-0.5千克=-500克"]);
        assert_eq!(rows("+0.5kg'g"), ["500克", "500", "+0.5千克=500克"]);
        assert_eq!(rows("-10c"), ["14℉", "263.15K"]);
    }

    #[test]
    fn signed_unit_measurements_keep_numeric_syntax_and_limits() {
        for sign in ["-", "+"] {
            assert!(query_units(&format!("{sign}1234567890123456cf")).is_some());
            assert!(query_units(&format!("{sign}12345678901234567cf")).is_none());
        }
        for code in [
            "-cf", "+cf", "--1cf", "+-1cf", "-+1cf", "++1cf", "1-2cf", "1+2cf", "-.5cf", "+.5cf",
            "-1.cf", "+1.cf", "-1.2.3cf", "-1e2cf", "-1kg'm",
        ] {
            assert_eq!(query_units(code), None, "{code}");
        }
    }

    #[test]
    fn temperature_queries_do_not_allocate_a_target_list() {
        for (code, expected, budget) in [
            ("100c", vec!["212℉", "373.15K"], 8),
            ("100cf", vec!["212℉", "212", "100℃=212℉"], 6),
        ] {
            let (actual, count) = allocations::count(|| query_units(code));
            assert_eq!(actual.unwrap(), expected);
            assert!(count <= budget, "{code}: {count} > {budget}");
        }
    }

    #[test]
    fn unit_queries_reserve_only_their_result_bound() {
        for (code, expected_capacity) in [("2jin", 3), ("100c", 2), ("1m", 7)] {
            let result = query_units(code).unwrap();
            assert_eq!(result.capacity(), expected_capacity, "{code}");
        }
    }

    #[test]
    fn a_unit_alone_lists_the_common_units_of_its_quantity() {
        let mu = rows("1mu");
        assert_eq!(
            mu,
            [
                "666.666666666667平方米",
                "0.0666666666666667公顷",
                "0.16473692097811英亩"
            ]
        );
        let jin = rows("2jin");
        assert_eq!(jin[..3], ["1000克", "1千克", "2.20462262184878磅"]);
        assert!(rows("1m").len() <= RESULT_LIMIT);
    }

    #[test]
    fn inputs_the_table_does_not_spell_are_left_to_the_expression_path() {
        for code in [
            "",
            "3",
            "1+2",
            "jin",
            "3xyz",
            "3jin'xyz",
            "3jin'kg'g",
            "3jin'm",
            "3cm'g",
            ".5jin",
            "5.jin",
            "1.2.3jin",
            "3jinjin",
            "3mm'mm",
        ] {
            assert_eq!(query_units(code), None, "{code:?}");
        }
    }

    #[test]
    fn every_table_unit_is_defined_at_its_statutory_value() {
        for source in UNITS.iter().filter(|unit| !unit.rink.is_empty()) {
            for target in UNITS
                .iter()
                .filter(|unit| unit.quantity == source.quantity && !std::ptr::eq(*unit, source))
            {
                assert!(
                    convert("1", 1.0, source, target).is_some(),
                    "{} -> {}",
                    source.rink,
                    target.rink
                );
            }
        }
        assert_eq!(rows("1t'kg")[0], "1000千克");
        assert_eq!(rows("1mg'g")[0], "0.001克");
        assert_eq!(rows("1oz'g")[0], "28.349523125克");
        assert_eq!(rows("1inch'mm")[0], "25.4毫米");
        assert_eq!(rows("1ft'cm")[0], "30.48厘米");
        assert_eq!(rows("1yd'm")[0], "0.9144米");
        assert_eq!(rows("1sqkm'ha")[0], "100公顷");
        assert_eq!(rows("1acre'sqm")[0], "4046.8564224平方米");
        assert_eq!(rows("1gal'ml")[0], "3785.411784毫升");
        assert_eq!(rows("1l'ml")[0], "1000毫升");
    }

    #[test]
    fn decimals_stay_exact() {
        assert_eq!(rows("1.5jin'g")[0], "750克");
        assert_eq!(rows("0.1kg'g")[0], "100克");
    }
}