use std::collections::HashMap;
use std::sync::OnceLock;
const TABLE: &str = include_str!("places.tsv");
const TABLE_LINE_COUNT: usize = {
let bytes = TABLE.as_bytes();
let mut count = 0;
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'\n' {
count += 1;
}
index += 1;
}
if !bytes.is_empty() && bytes[bytes.len() - 1] != b'\n' {
count += 1;
}
count
};
const MATCH_MASK_WORDS: usize = TABLE_LINE_COUNT.div_ceil(u64::BITS as usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Place {
pub name: &'static str,
pub key: &'static str,
pub parent: &'static str,
}
pub struct Places {
pub places: Vec<Place>,
pub spellings: Vec<[String; 2]>,
spelling_index: Vec<usize>,
parents: HashMap<&'static str, &'static str>,
}
impl Places {
fn parse(table: &'static str) -> Self {
let mut places = Vec::with_capacity(table.lines().count());
for line in table.lines() {
let mut fields = line.split('\t');
if let (Some(name), Some(key), Some(parent), None) =
(fields.next(), fields.next(), fields.next(), fields.next())
{
places.push(Place { name, key, parent });
}
}
assert!(
places.len() <= TABLE_LINE_COUNT,
"地点索引只用于内置表容量范围"
);
let spellings: Vec<[String; 2]> = places
.iter()
.map(|place| {
[
place.key.replace('\'', ""),
place
.key
.split('\'')
.filter_map(|syllable| syllable.chars().next())
.collect(),
]
})
.collect();
let mut spelling_index: Vec<usize> = (0..spellings.len() * 2).collect();
spelling_index.sort_unstable_by(|&left, &right| {
spellings[left / 2][left % 2]
.cmp(&spellings[right / 2][right % 2])
.then_with(|| left.cmp(&right))
});
let parents = places
.iter()
.map(|place| (place.name, place.parent))
.collect();
Self {
places,
spellings,
spelling_index,
parents,
}
}
pub(super) fn matching_entries(
&self,
code: &str,
) -> impl Iterator<Item = (&Place, &[String; 2])> {
let start = self
.spelling_index
.partition_point(|&index| self.spellings[index / 2][index % 2].as_str() < code);
let mut masks = [0_u64; MATCH_MASK_WORDS];
let mut used_words = 0;
for &index in self.spelling_index[start..]
.iter()
.take_while(|&&index| self.spellings[index / 2][index % 2].starts_with(code))
{
if self.spellings[index / 2][index % 2].is_empty() {
continue;
}
let row = index / 2;
let word = row / u64::BITS as usize;
masks[word] |= 1_u64 << (row % u64::BITS as usize);
used_words = used_words.max(word + 1);
}
masks
.into_iter()
.take(used_words)
.enumerate()
.flat_map(|(word, mut bits)| {
std::iter::from_fn(move || {
if bits == 0 {
return None;
}
let row = word * u64::BITS as usize + bits.trailing_zeros() as usize;
bits &= bits - 1;
Some(row)
})
})
.map(|row| (&self.places[row], &self.spellings[row]))
}
pub fn parent(&self, name: &str) -> Option<&'static str> {
self.parents.get(name).copied()
}
}
pub fn places() -> &'static Places {
static PLACES: OnceLock<Places> = OnceLock::new();
PLACES.get_or_init(|| Places::parse(TABLE))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_embedded_table_is_well_formed() {
let table = places();
assert_eq!(table.places.len(), TABLE.lines().count());
assert_eq!(table.places.capacity(), TABLE.lines().count());
assert!(table.places.len() > 3000);
assert_eq!(table.parents.len(), table.places.len());
for place in &table.places {
assert!(!place.name.trim().is_empty(), "{place:?}");
assert!(
place.key.split('\'').all(|syllable| !syllable.is_empty()
&& syllable.bytes().all(|byte| byte.is_ascii_lowercase())),
"{place:?}"
);
assert!(
place.parent.is_empty() || table.parent(place.parent).is_some(),
"{place:?}"
);
}
}
#[test]
fn places_carry_their_reading_and_parent() {
let table = places();
let find = |name: &str| {
*table
.places
.iter()
.find(|place| place.name == name)
.unwrap()
};
assert_eq!(
find("重庆市"),
Place {
name: "重庆市",
key: "chong'qing'shi",
parent: ""
}
);
assert_eq!(find("六安市").key, "lu'an'shi");
assert_eq!(find("厦门市").key, "xia'men'shi");
assert_eq!(table.parent("深圳市"), Some("广东省"));
assert_eq!(table.parent("朝阳区"), Some("北京市"));
assert_eq!(table.parent("张三"), None);
assert_eq!(table.places[0].name, "北京市");
}
}
#[cfg(test)]
#[path = "places/prefix_index_tests.rs"]
mod prefix_index_tests;