use rustc_hash::{FxHashMap, FxHashSet};
#[derive(Clone, Debug, PartialEq, Eq)]
enum SubstitutionSetImpl {
Small(Vec<(u8, u8)>),
Large(FxHashSet<(u8, u8)>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum MultiCharSubstitutionImpl {
Small(Vec<(Box<str>, Box<str>)>),
Large(FxHashMap<Box<str>, FxHashSet<Box<str>>>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubstitutionSet {
inner: SubstitutionSetImpl,
multi_char: MultiCharSubstitutionImpl,
}
impl SubstitutionSet {
const SMALL_SET_THRESHOLD: usize = 4;
#[inline]
pub fn new() -> Self {
Self {
inner: SubstitutionSetImpl::Small(Vec::new()),
multi_char: MultiCharSubstitutionImpl::Small(Vec::new()),
}
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
if capacity <= Self::SMALL_SET_THRESHOLD {
Self {
inner: SubstitutionSetImpl::Small(Vec::with_capacity(capacity)),
multi_char: MultiCharSubstitutionImpl::Small(Vec::new()),
}
} else {
Self {
inner: SubstitutionSetImpl::Large(FxHashSet::with_capacity_and_hasher(
capacity,
Default::default(),
)),
multi_char: MultiCharSubstitutionImpl::Small(Vec::new()),
}
}
}
#[inline]
pub fn allow(&mut self, a: char, b: char) {
if a.is_ascii() && b.is_ascii() {
self.allow_byte(a as u8, b as u8);
}
}
#[inline]
pub fn allow_byte(&mut self, a: u8, b: u8) {
match &mut self.inner {
SubstitutionSetImpl::Small(vec) if vec.len() < Self::SMALL_SET_THRESHOLD => {
if !vec.contains(&(a, b)) {
vec.push((a, b));
}
}
SubstitutionSetImpl::Small(vec) => {
let mut set =
FxHashSet::with_capacity_and_hasher(vec.len() + 1, Default::default());
for &pair in vec.iter() {
set.insert(pair);
}
set.insert((a, b));
self.inner = SubstitutionSetImpl::Large(set);
}
SubstitutionSetImpl::Large(set) => {
set.insert((a, b));
}
}
}
#[inline]
pub fn contains(&self, a: u8, b: u8) -> bool {
match &self.inner {
SubstitutionSetImpl::Small(vec) => {
vec.iter().any(|&(x, y)| x == a && y == b)
}
SubstitutionSetImpl::Large(set) => {
set.contains(&(a, b))
}
}
}
pub fn from_pairs(pairs: &[(char, char)]) -> Self {
let mut set = Self::with_capacity(pairs.len());
for &(a, b) in pairs {
set.allow(a, b);
}
set
}
#[inline]
pub fn len(&self) -> usize {
match &self.inner {
SubstitutionSetImpl::Small(vec) => vec.len(),
SubstitutionSetImpl::Large(set) => set.len(),
}
}
#[inline]
pub fn is_empty(&self) -> bool {
match &self.inner {
SubstitutionSetImpl::Small(vec) => vec.is_empty(),
SubstitutionSetImpl::Large(set) => set.is_empty(),
}
}
#[inline]
pub fn clear(&mut self) {
match &mut self.inner {
SubstitutionSetImpl::Small(vec) => vec.clear(),
SubstitutionSetImpl::Large(set) => set.clear(),
}
match &mut self.multi_char {
MultiCharSubstitutionImpl::Small(vec) => vec.clear(),
MultiCharSubstitutionImpl::Large(map) => map.clear(),
}
}
const PHONETIC_PAIRS: &[(u8, u8)] = &[
(b'f', b'p'),
(b'p', b'f'),
(b'c', b'k'),
(b'k', b'c'),
(b'c', b's'),
(b's', b'c'),
(b's', b'z'),
(b'z', b's'),
(b'a', b'e'),
(b'e', b'a'),
(b'i', b'y'),
(b'y', b'i'),
(b'h', b'\0'),
(b'k', b'\0'),
];
const KEYBOARD_PAIRS: &[(u8, u8)] = &[
(b'q', b'w'),
(b'w', b'q'),
(b'w', b'e'),
(b'e', b'w'),
(b'e', b'r'),
(b'r', b'e'),
(b'r', b't'),
(b't', b'r'),
(b't', b'y'),
(b'y', b't'),
(b'y', b'u'),
(b'u', b'y'),
(b'u', b'i'),
(b'i', b'u'),
(b'i', b'o'),
(b'o', b'i'),
(b'o', b'p'),
(b'p', b'o'),
(b'a', b's'),
(b's', b'a'),
(b's', b'd'),
(b'd', b's'),
(b'd', b'f'),
(b'f', b'd'),
(b'f', b'g'),
(b'g', b'f'),
(b'g', b'h'),
(b'h', b'g'),
(b'h', b'j'),
(b'j', b'h'),
(b'j', b'k'),
(b'k', b'j'),
(b'k', b'l'),
(b'l', b'k'),
(b'z', b'x'),
(b'x', b'z'),
(b'x', b'c'),
(b'c', b'x'),
(b'c', b'v'),
(b'v', b'c'),
(b'v', b'b'),
(b'b', b'v'),
(b'b', b'n'),
(b'n', b'b'),
(b'n', b'm'),
(b'm', b'n'),
(b'q', b'a'),
(b'a', b'q'),
(b'w', b's'),
(b's', b'w'),
(b'e', b'd'),
(b'd', b'e'),
(b'r', b'f'),
(b'f', b'r'),
(b't', b'g'),
(b'g', b't'),
(b'y', b'h'),
(b'h', b'y'),
(b'u', b'j'),
(b'j', b'u'),
(b'i', b'k'),
(b'k', b'i'),
(b'o', b'l'),
(b'l', b'o'),
];
const LEET_PAIRS: &[(u8, u8)] = &[
(b'e', b'3'),
(b'3', b'e'),
(b'a', b'@'),
(b'@', b'a'),
(b'a', b'4'),
(b'4', b'a'),
(b'o', b'0'),
(b'0', b'o'),
(b'i', b'1'),
(b'1', b'i'),
(b'l', b'1'),
(b'1', b'l'),
(b's', b'$'),
(b'$', b's'),
(b's', b'5'),
(b'5', b's'),
(b't', b'7'),
(b'7', b't'),
(b'b', b'8'),
(b'8', b'b'),
(b'g', b'9'),
(b'9', b'g'),
];
const OCR_PAIRS: &[(u8, u8)] = &[
(b'0', b'O'),
(b'O', b'0'),
(b'0', b'o'),
(b'o', b'0'),
(b'1', b'I'),
(b'I', b'1'),
(b'1', b'l'),
(b'l', b'1'),
(b'I', b'l'),
(b'l', b'I'),
(b'8', b'B'),
(b'B', b'8'),
(b'5', b'S'),
(b'S', b'5'),
(b'6', b'G'),
(b'G', b'6'),
(b'2', b'Z'),
(b'Z', b'2'),
];
pub fn phonetic_basic() -> Self {
let mut set = Self::with_capacity(Self::PHONETIC_PAIRS.len());
for &(a, b) in Self::PHONETIC_PAIRS {
set.allow_byte(a, b);
}
set
}
pub fn keyboard_qwerty() -> Self {
let mut set = Self::with_capacity(Self::KEYBOARD_PAIRS.len());
for &(a, b) in Self::KEYBOARD_PAIRS {
set.allow_byte(a, b);
}
set
}
pub fn leet_speak() -> Self {
let mut set = Self::with_capacity(Self::LEET_PAIRS.len());
for &(a, b) in Self::LEET_PAIRS {
set.allow_byte(a, b);
}
set
}
pub fn ocr_friendly() -> Self {
let mut set = Self::with_capacity(Self::OCR_PAIRS.len());
for &(a, b) in Self::OCR_PAIRS {
set.allow_byte(a, b);
}
set
}
pub fn allow_str(&mut self, a: &str, b: &str) {
if a.is_empty() || b.is_empty() {
return;
}
if a.len() == 1 && b.len() == 1 {
if let (Some(a_char), Some(b_char)) = (a.chars().next(), b.chars().next()) {
if a_char.is_ascii() && b_char.is_ascii() {
self.allow_byte(a_char as u8, b_char as u8);
return;
}
}
}
if !a.is_ascii() || !b.is_ascii() {
return;
}
let a_boxed: Box<str> = a.into();
let b_boxed: Box<str> = b.into();
match &mut self.multi_char {
MultiCharSubstitutionImpl::Small(vec) if vec.len() < Self::SMALL_SET_THRESHOLD => {
if !vec.iter().any(|(x, y)| x.as_ref() == a && y.as_ref() == b) {
vec.push((a_boxed, b_boxed));
}
}
MultiCharSubstitutionImpl::Small(vec) => {
let mut map: FxHashMap<Box<str>, FxHashSet<Box<str>>> =
FxHashMap::with_capacity_and_hasher(vec.len() + 1, Default::default());
for (src, tgt) in vec.drain(..) {
map.entry(src)
.or_insert_with(|| {
FxHashSet::with_capacity_and_hasher(1, Default::default())
})
.insert(tgt);
}
map.entry(a_boxed)
.or_insert_with(|| FxHashSet::with_capacity_and_hasher(1, Default::default()))
.insert(b_boxed);
self.multi_char = MultiCharSubstitutionImpl::Large(map);
}
MultiCharSubstitutionImpl::Large(map) => {
map.entry(a_boxed)
.or_insert_with(|| FxHashSet::with_capacity_and_hasher(1, Default::default()))
.insert(b_boxed);
}
}
}
#[inline]
pub fn contains_str(&self, a: &[u8], b: &[u8]) -> bool {
if a.len() == 1 && b.len() == 1 {
return self.contains(a[0], b[0]);
}
let a_str = match std::str::from_utf8(a) {
Ok(s) if s.is_ascii() => s,
_ => return false,
};
let b_str = match std::str::from_utf8(b) {
Ok(s) if s.is_ascii() => s,
_ => return false,
};
match &self.multi_char {
MultiCharSubstitutionImpl::Small(vec) => {
vec.iter()
.any(|(x, y)| x.as_ref() == a_str && y.as_ref() == b_str)
}
MultiCharSubstitutionImpl::Large(map) => {
map.get(a_str)
.map(|targets| targets.contains(b_str))
.unwrap_or(false)
}
}
}
#[inline]
pub fn has_source(&self, source: &[u8]) -> bool {
let src_str = match std::str::from_utf8(source) {
Ok(s) if s.is_ascii() => s,
_ => return false,
};
if source.len() == 1 {
let src_byte = source[0];
let found_in_single = match &self.inner {
SubstitutionSetImpl::Small(vec) => vec.iter().any(|(s, _)| *s == src_byte),
SubstitutionSetImpl::Large(set) => {
set.iter().any(|(s, _)| *s == src_byte)
}
};
if found_in_single {
return true;
}
}
match &self.multi_char {
MultiCharSubstitutionImpl::Small(vec) => vec.iter().any(|(s, _)| s.as_ref() == src_str),
MultiCharSubstitutionImpl::Large(map) => {
map.contains_key(src_str)
}
}
}
#[inline]
pub fn has_target_starting_with(&self, source: &[u8], first_char: char) -> bool {
let src_str = match std::str::from_utf8(source) {
Ok(s) if s.is_ascii() => s,
_ => return false,
};
if source.len() == 1 {
let src_byte = source[0];
let found_in_single = match &self.inner {
SubstitutionSetImpl::Small(vec) => {
vec.iter()
.filter(|(s, _)| *s == src_byte)
.any(|(_, target)| {
let target_char = *target as char;
target_char == first_char
})
}
SubstitutionSetImpl::Large(set) => {
set.iter()
.filter(|(s, _)| *s == src_byte)
.any(|(_, target)| {
let target_char = *target as char;
target_char == first_char
})
}
};
if found_in_single {
return true;
}
}
match &self.multi_char {
MultiCharSubstitutionImpl::Small(vec) => {
vec.iter()
.filter(|(s, _)| s.as_ref() == src_str)
.any(|(_, target)| {
target.chars().next() == Some(first_char)
})
}
MultiCharSubstitutionImpl::Large(map) => {
map.get(src_str)
.map(|target_set| {
target_set
.iter()
.any(|target| target.chars().next() == Some(first_char))
})
.unwrap_or(false)
}
}
}
pub fn from_str_pairs(pairs: &[(&str, &str)]) -> Self {
let mut set = Self::with_capacity(pairs.len());
for &(a, b) in pairs {
set.allow_str(a, b);
}
set
}
}
impl Default for SubstitutionSet {
#[inline]
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_empty() {
let set = SubstitutionSet::new();
assert_eq!(set.len(), 0);
assert!(set.is_empty());
}
#[test]
fn test_allow_and_contains() {
let mut set = SubstitutionSet::new();
set.allow('a', 'b');
assert!(set.contains(b'a', b'b'));
assert!(!set.contains(b'b', b'a'));
set.allow('b', 'a'); assert!(set.contains(b'b', b'a'));
}
#[test]
fn test_allow_byte() {
let mut set = SubstitutionSet::new();
set.allow_byte(b'x', b'y');
assert!(set.contains(b'x', b'y'));
assert_eq!(set.len(), 1);
}
#[test]
fn test_from_pairs() {
let set = SubstitutionSet::from_pairs(&[('a', 'b'), ('c', 'd'), ('e', 'f')]);
assert_eq!(set.len(), 3);
assert!(set.contains(b'a', b'b'));
assert!(set.contains(b'c', b'd'));
assert!(set.contains(b'e', b'f'));
}
#[test]
fn test_clear() {
let mut set = SubstitutionSet::from_pairs(&[('a', 'b'), ('c', 'd')]);
assert_eq!(set.len(), 2);
set.clear();
assert_eq!(set.len(), 0);
assert!(set.is_empty());
}
#[test]
fn test_phonetic_basic() {
let phonetic = SubstitutionSet::phonetic_basic();
assert!(!phonetic.is_empty());
assert!(phonetic.contains(b'f', b'p'));
assert!(phonetic.contains(b'p', b'f'));
assert!(phonetic.contains(b'c', b'k'));
assert!(phonetic.contains(b'k', b'c'));
assert!(phonetic.contains(b's', b'z'));
assert!(phonetic.contains(b'z', b's'));
}
#[test]
fn test_keyboard_qwerty() {
let keyboard = SubstitutionSet::keyboard_qwerty();
assert!(!keyboard.is_empty());
assert!(keyboard.contains(b'q', b'w'));
assert!(keyboard.contains(b'w', b'q'));
assert!(keyboard.contains(b'a', b's'));
assert!(keyboard.contains(b's', b'a'));
assert!(keyboard.contains(b'q', b'a'));
assert!(keyboard.contains(b'a', b'q'));
}
#[test]
fn test_leet_speak() {
let leet = SubstitutionSet::leet_speak();
assert!(!leet.is_empty());
assert!(leet.contains(b'e', b'3'));
assert!(leet.contains(b'3', b'e'));
assert!(leet.contains(b'a', b'@'));
assert!(leet.contains(b'@', b'a'));
assert!(leet.contains(b'o', b'0'));
assert!(leet.contains(b'0', b'o'));
}
#[test]
fn test_ocr_friendly() {
let ocr = SubstitutionSet::ocr_friendly();
assert!(!ocr.is_empty());
assert!(ocr.contains(b'0', b'O'));
assert!(ocr.contains(b'O', b'0'));
assert!(ocr.contains(b'1', b'I'));
assert!(ocr.contains(b'1', b'l'));
assert!(ocr.contains(b'I', b'l'));
}
#[test]
fn test_with_capacity() {
let set = SubstitutionSet::with_capacity(100);
assert_eq!(set.len(), 0);
}
#[test]
fn test_non_ascii_ignored() {
let mut set = SubstitutionSet::new();
set.allow('α', 'β'); set.allow('你', '好');
assert_eq!(set.len(), 0); }
#[test]
fn test_duplicate_pairs() {
let mut set = SubstitutionSet::new();
set.allow('a', 'b');
set.allow('a', 'b');
assert_eq!(set.len(), 1);
}
#[test]
fn test_clone() {
let set1 = SubstitutionSet::phonetic_basic();
let set2 = set1.clone();
assert_eq!(set1.len(), set2.len());
assert_eq!(set1, set2);
}
#[test]
fn test_debug() {
let set = SubstitutionSet::from_pairs(&[('a', 'b')]);
let debug_str = format!("{:?}", set);
assert!(debug_str.contains("SubstitutionSet"));
}
#[test]
fn test_multi_char_basic() {
let mut set = SubstitutionSet::new();
set.allow_str("ph", "f");
set.allow_str("ch", "k");
assert!(set.contains_str(b"ph", b"f"));
assert!(set.contains_str(b"ch", b"k"));
assert!(!set.contains_str(b"f", b"ph"));
assert!(!set.contains_str(b"k", b"ch"));
let set2 = SubstitutionSet::from_str_pairs(&[("ph", "f"), ("ch", "k")]);
assert!(set2.contains_str(b"ph", b"f"));
assert!(set2.contains_str(b"ch", b"k"));
}
#[test]
fn test_multi_char_digraphs() {
let mut set = SubstitutionSet::new();
set.allow_str("ph", "f");
set.allow_str("ch", "k"); set.allow_str("sh", "$");
set.allow_str("th", "+");
assert!(set.contains_str(b"ph", b"f"));
assert!(set.contains_str(b"ch", b"k"));
assert!(set.contains_str(b"sh", b"$"));
assert!(set.contains_str(b"th", b"+"));
assert!(!set.contains_str(b"ph", b"g"));
assert!(!set.contains_str(b"xy", b"z"));
}
#[test]
fn test_multi_char_trigraphs() {
let mut set = SubstitutionSet::new();
set.allow_str("eau", "o"); set.allow_str("ght", "t");
assert!(set.contains_str(b"eau", b"o"));
assert!(set.contains_str(b"ght", b"t"));
}
#[test]
fn test_multi_char_one_to_many() {
let mut set = SubstitutionSet::new();
set.allow_str("c", "k");
set.allow_str("c", "s");
assert!(set.contains_str(b"c", b"k"));
assert!(set.contains_str(b"c", b"s"));
}
#[test]
fn test_multi_char_mixed_with_single() {
let mut set = SubstitutionSet::new();
set.allow('f', 'p'); set.allow_str("ph", "f");
assert!(set.contains(b'f', b'p'));
assert!(set.contains_str(b"ph", b"f"));
assert!(set.contains_str(b"f", b"p"));
}
#[test]
fn test_multi_char_upgrade_to_hashmap() {
let mut set = SubstitutionSet::new();
set.allow_str("ph", "f");
set.allow_str("ch", "k");
set.allow_str("sh", "$");
set.allow_str("th", "+");
set.allow_str("wh", "w");
assert!(set.contains_str(b"ph", b"f"));
assert!(set.contains_str(b"ch", b"k"));
assert!(set.contains_str(b"sh", b"$"));
assert!(set.contains_str(b"th", b"+"));
assert!(set.contains_str(b"wh", b"w"));
}
#[test]
fn test_multi_char_duplicates() {
let mut set = SubstitutionSet::new();
set.allow_str("ph", "f");
set.allow_str("ph", "f");
assert!(set.contains_str(b"ph", b"f"));
}
#[test]
fn test_multi_char_clear() {
let mut set = SubstitutionSet::new();
set.allow_str("ph", "f");
set.allow_str("ch", "k");
assert!(set.contains_str(b"ph", b"f"));
set.clear();
assert!(!set.contains_str(b"ph", b"f"));
assert!(!set.contains_str(b"ch", b"k"));
}
#[test]
fn test_multi_char_non_ascii_ignored() {
let mut set = SubstitutionSet::new();
set.allow_str("α", "β"); set.allow_str("你", "好");
assert!(!set.contains_str("α".as_bytes(), "β".as_bytes()));
assert!(!set.contains_str("你".as_bytes(), "好".as_bytes()));
}
#[test]
fn test_multi_char_empty_strings() {
let mut set = SubstitutionSet::new();
set.allow_str("", ""); set.allow_str("a", ""); set.allow_str("", "b");
assert!(!set.contains_str(b"", b""));
assert!(!set.contains_str(b"a", b""));
assert!(!set.contains_str(b"", b"b"));
assert!(set.is_empty());
}
#[test]
fn test_has_source_single_char() {
let mut set = SubstitutionSet::new();
set.allow('f', 'p');
set.allow('k', 'c');
assert!(set.has_source(b"f"));
assert!(set.has_source(b"k"));
assert!(!set.has_source(b"a"));
assert!(!set.has_source(b"p")); }
#[test]
fn test_has_source_multi_char() {
let mut set = SubstitutionSet::new();
set.allow_str("ph", "f");
set.allow_str("ch", "k");
set.allow_str("k", "ch");
assert!(set.has_source(b"ph"));
assert!(set.has_source(b"ch"));
assert!(set.has_source(b"k"));
assert!(!set.has_source(b"f")); assert!(!set.has_source(b"th")); }
#[test]
fn test_has_source_mixed() {
let mut set = SubstitutionSet::new();
set.allow('f', 'p');
set.allow_str("ph", "f");
set.allow_str("k", "ch");
assert!(set.has_source(b"f")); assert!(set.has_source(b"ph")); assert!(set.has_source(b"k")); assert!(!set.has_source(b"p")); assert!(!set.has_source(b"ch")); }
#[test]
fn test_has_source_upgrade_to_large() {
let mut set = SubstitutionSet::new();
set.allow_str("a", "b");
set.allow_str("c", "d");
set.allow_str("e", "f");
set.allow_str("g", "h");
set.allow_str("i", "j");
assert!(set.has_source(b"a"));
assert!(set.has_source(b"c"));
assert!(set.has_source(b"e"));
assert!(set.has_source(b"g"));
assert!(set.has_source(b"i"));
assert!(!set.has_source(b"b")); }
#[test]
fn test_has_source_empty() {
let set = SubstitutionSet::new();
assert!(!set.has_source(b"a"));
assert!(!set.has_source(b"ph"));
}
#[test]
fn test_has_source_phonetic_example() {
let mut set = SubstitutionSet::new();
set.allow_str("k", "ch");
set.allow_str("s", "sh");
set.allow_str("f", "ph");
set.allow_str("t", "th");
assert!(set.has_source(b"k")); assert!(set.has_source(b"s")); assert!(set.has_source(b"f")); assert!(set.has_source(b"t")); assert!(!set.has_source(b"a")); assert!(!set.has_source(b"e")); }
#[test]
fn test_has_target_starting_with_single_char_to_multi_char() {
let mut set = SubstitutionSet::new();
set.allow_str("k", "ch");
set.allow_str("s", "sh");
set.allow_str("t", "th");
assert!(set.has_target_starting_with(b"k", 'c'));
assert!(!set.has_target_starting_with(b"k", 's'));
assert!(!set.has_target_starting_with(b"k", 't'));
assert!(set.has_target_starting_with(b"s", 's'));
assert!(!set.has_target_starting_with(b"s", 'c'));
assert!(set.has_target_starting_with(b"t", 't'));
assert!(!set.has_target_starting_with(b"t", 'c'));
}
#[test]
fn test_has_target_starting_with_reverse_operations() {
let mut set = SubstitutionSet::new();
set.allow_str("ch", "k");
set.allow_str("sh", "s");
set.allow_str("th", "t");
assert!(set.has_target_starting_with(b"ch", 'k'));
assert!(!set.has_target_starting_with(b"ch", 's'));
assert!(set.has_target_starting_with(b"sh", 's'));
assert!(!set.has_target_starting_with(b"sh", 'k'));
}
#[test]
fn test_has_target_starting_with_multiple_targets() {
let mut set = SubstitutionSet::new();
set.allow_str("a", "b");
set.allow_str("a", "c");
set.allow_str("a", "d");
assert!(set.has_target_starting_with(b"a", 'b'));
assert!(set.has_target_starting_with(b"a", 'c'));
assert!(set.has_target_starting_with(b"a", 'd'));
assert!(!set.has_target_starting_with(b"a", 'e'));
}
#[test]
fn test_has_target_starting_with_nonexistent_source() {
let mut set = SubstitutionSet::new();
set.allow_str("k", "ch");
assert!(!set.has_target_starting_with(b"x", 'c'));
assert!(!set.has_target_starting_with(b"y", 'y'));
}
#[test]
fn test_has_target_starting_with_empty_set() {
let set = SubstitutionSet::new();
assert!(!set.has_target_starting_with(b"k", 'c'));
assert!(!set.has_target_starting_with(b"a", 'b'));
}
#[test]
fn test_has_target_starting_with_realistic_phonetic() {
let mut set = SubstitutionSet::new();
set.allow_str("k", "ch");
set.allow_str("s", "sh");
set.allow_str("f", "ph");
set.allow_str("t", "th");
set.allow_str("ch", "k");
set.allow_str("sh", "s");
set.allow_str("ph", "f");
set.allow_str("th", "t");
assert!(set.has_target_starting_with(b"k", 'c')); assert!(set.has_target_starting_with(b"t", 't')); assert!(!set.has_target_starting_with(b"t", 'a')); assert!(!set.has_target_starting_with(b"k", 'a'));
assert!(set.has_target_starting_with(b"ch", 'k')); assert!(set.has_target_starting_with(b"th", 't')); }
}