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};
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
";
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,
}
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,
}
}
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();
pub fn warm_up() {
context();
}
pub fn warm_up_in_background() {
WARM_UP.call_once(|| {
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()
}
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)
}
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)
}
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?;
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克");
}
}