use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
use unicode_width::UnicodeWidthStr;
pub fn grapheme_at_idx(st: &str, idx: usize) -> &str {
let pos = grapheme_idx_at_idx(&st, idx);
&st[pos..pos + st[pos..].graphemes(true).next().unwrap_or("").len()]
}
pub fn grapheme_len(st: &str, idx: usize) -> usize {
let pos = grapheme_idx_at_idx(&st, idx);
st[pos..].graphemes(true).next().unwrap_or("").len()
}
pub fn grapheme_idx_at_idx(st: &str, idx: usize) -> usize {
if idx == 0 {
return 0;
}
let mut pos = idx;
if idx >= st.len() {
return st.len();
}
let mut cursor = GraphemeCursor::new(idx, st.len(), true);
loop {
while pos > 0 && (st.as_bytes()[pos] & 0xc0) == 0x80 {
pos -= 1;
}
cursor.set_cursor(pos);
if cursor.is_boundary(st, 0).unwrap_or(false) {
break;
}
pos -= 1;
}
pos
}
pub fn grapheme_width_at_idx(st: &str, idx: usize) -> usize {
let pos = grapheme_idx_at_idx(&st, idx);
st[pos..].graphemes(true).next().unwrap_or("").width()
}
pub fn next_grapheme_from_idx(st: &str, idx: usize) -> &str {
let st_len = st.len();
if idx >= st_len {
return "";
}
let pos = next_grapheme_idx_from_idx(&st, idx);
st[pos..].graphemes(true).next().unwrap_or("")
}
pub fn next_grapheme_idx_from_idx(st: &str, idx: usize) -> usize {
let st_len = st.len();
if idx >= st_len {
return st_len;
}
let mut pos = idx;
while pos > 0 && (st.as_bytes()[pos] & 0xc0) == 0x80 {
pos -= 1;
}
let mut cursor = GraphemeCursor::new(pos, st_len, true);
cursor
.next_boundary(st, 0)
.ok()
.flatten()
.unwrap_or_else(|| st_len)
}
pub fn nth_grapheme(st: &str, nth: usize) -> &str {
UnicodeSegmentation::grapheme_indices(st, true)
.nth(nth)
.map(|(_, g)| g)
.unwrap_or_else(|| "")
}
pub fn nth_grapheme_idx(st: &str, nth: usize) -> usize {
UnicodeSegmentation::grapheme_indices(st, true)
.nth(nth)
.map(|(idx, _)| idx)
.unwrap_or_else(|| st.len())
}
pub fn nth_grapheme_width(st: &str, nth: usize) -> usize {
UnicodeSegmentation::grapheme_indices(st, true)
.nth(nth)
.map(|(_, g)| g)
.unwrap_or_else(|| "")
.width()
}
pub fn num_graphemes(st: &str) -> usize {
UnicodeSegmentation::grapheme_indices(st, true).count()
}
pub fn prev_grapheme_from_idx(st: &str, idx: usize) -> &str {
if idx == 0 {
return "";
}
let pos = prev_grapheme_idx_from_idx(&st, idx);
grapheme_at_idx(&st, pos)
}
pub fn prev_grapheme_idx_from_idx(st: &str, idx: usize) -> usize {
let st_len = st.len();
if st_len == 0 {
return 0;
}
let max_len = st_len.saturating_sub(1);
let mut pos = idx;
while pos <= max_len && (st.as_bytes()[pos] & 0xc0) == 0x80 {
pos += 1;
}
if pos > st_len {
pos = st_len;
}
let mut cursor = GraphemeCursor::new(pos, st_len, true);
let pos = match cursor.prev_boundary(st, 0) {
Ok(Some(prev)) => prev,
_ => 0, };
pos
}
pub fn string_width(st: &str) -> usize {
let mut total = 0;
for (_, grapheme) in st.grapheme_indices(true) {
total += grapheme.width();
}
total
}
#[cfg(test)]
mod tests {
use super::*;
type TestData = (
usize, usize, usize, usize, &'static str, &'static str, &'static str, usize, usize, &'static str, usize, usize, usize, usize, );
fn run_grapheme_test(st: &str, expected: Vec<TestData>) {
let string_len = st.len();
println!("Testing grapheme vector for \"{}\"", st);
assert_eq!(expected.len(), string_len + 2);
for i in 0..string_len + 2 {
print!("Testing String: \"{}\", at byte index: {} \n", st, i);
assert_eq!(expected[i].0, i); print!("i:{} ok, \n", i);
let pgifi = prev_grapheme_idx_from_idx(st, i);
print!("prev_grapheme_idx_from_idx");
assert_eq!(expected[i].1, pgifi);
println!(" ok:{}", pgifi);
let giati = grapheme_idx_at_idx(st, i);
print!("grapheme_idx_at_idx");
assert_eq!(expected[i].2, giati);
println!(" ok:{}", giati);
let nxgifi = next_grapheme_idx_from_idx(st, i);
print!("next_grapheme_idx_from_idx");
assert_eq!(expected[i].3, nxgifi);
println!(" ok:{}\n", nxgifi);
let pgfi = prev_grapheme_from_idx(st, i);
print!("prev_grapheme_from_idx");
assert_eq!(expected[i].4, pgfi);
println!(" ok:{}", pgfi);
let gati = grapheme_at_idx(st, i);
print!("grapheme_at_idx");
assert_eq!(expected[i].5, gati);
println!(" ok:{}", gati);
let nxgfi = next_grapheme_from_idx(st, i);
print!("next_grapheme_from_idx");
assert_eq!(expected[i].6, nxgfi);
println!(" ok:{}\n", nxgfi);
let gwi = grapheme_width_at_idx(st, i);
print!("grapheme_width_from_idx");
assert_eq!(expected[i].7, gwi);
println!(" ok:{}", gwi);
let glen = grapheme_len(st, i);
print!("grapheme_len");
assert_eq!(expected[i].8, glen);
println!(" ok:{}", glen);
let nthg = nth_grapheme(st, i);
print!("nth_grapheme");
assert_eq!(expected[i].9, nthg);
println!(" ok:{}", nthg);
let ngw = nth_grapheme_width(st, i);
print!("nth grapheme_width");
assert_eq!(expected[i].10, ngw);
println!(" ok:{}", ngw);
let nthgi = nth_grapheme_idx(st, i);
print!("nth_grapheme_idx");
assert_eq!(expected[i].11, nthgi);
println!(" ok:{}", nthgi);
let numg = num_graphemes(st);
print!("num_graphemes");
assert_eq!(expected[i].12, numg);
println!(" ok:{}", numg);
let sw = string_width(st);
print!("string_width");
assert_eq!(expected[i].13, sw);
println!(" ok:{}", sw);
}
}
#[test]
fn test_grapheme_vectors() {
let test_cases: Vec<(String, Vec<TestData>)> = vec![
(
"".to_string(),
vec![
(0, 0, 0, 0, "", "", "", 0, 0, "", 0, 0, 0, 0),
(1, 0, 0, 0, "", "", "", 0, 0, "", 0, 0, 0, 0),
],
),
(
"é".to_string(),
vec![
(0, 0, 0, 2, "", "é", "", 1, 2, "é", 1, 0, 1, 1),
(1, 0, 0, 2, "é", "é", "", 1, 2, "", 0, 2, 1, 1),
(2, 0, 2, 2, "é", "", "", 0, 0, "", 0, 2, 1, 1),
(3, 0, 2, 2, "é", "", "", 0, 0, "", 0, 2, 1, 1),
],
),
(
"é".to_string(),
vec![
(0, 0, 0, 3, "", "é", "", 1, 3, "é", 1, 0, 1, 1),
(1, 0, 0, 3, "é", "é", "", 1, 3, "", 0, 3, 1, 1),
(2, 0, 0, 3, "é", "é", "", 1, 3, "", 0, 3, 1, 1),
(3, 0, 3, 3, "é", "", "", 0, 0, "", 0, 3, 1, 1),
(4, 0, 3, 3, "é", "", "", 0, 0, "", 0, 3, 1, 1),
],
),
(
"aé".to_string(),
vec![
(0, 0, 0, 1, "", "a", "é", 1, 1, "a", 1, 0, 2, 2),
(1, 0, 1, 4, "a", "é", "", 1, 3, "é", 1, 1, 2, 2),
(2, 1, 1, 4, "é", "é", "", 1, 3, "", 0, 4, 2, 2),
(3, 1, 1, 4, "é", "é", "", 1, 3, "", 0, 4, 2, 2),
(4, 1, 4, 4, "é", "", "", 0, 0, "", 0, 4, 2, 2),
(5, 1, 4, 4, "é", "", "", 0, 0, "", 0, 4, 2, 2),
],
),
(
"aé".to_string(),
vec![
(0, 0, 0, 1, "", "a", "é", 1, 1, "a", 1, 0, 2, 2),
(1, 0, 1, 3, "a", "é", "", 1, 2, "é", 1, 1, 2, 2),
(2, 1, 1, 3, "é", "é", "", 1, 2, "", 0, 3, 2, 2),
(3, 1, 3, 3, "é", "", "", 0, 0, "", 0, 3, 2, 2),
(4, 1, 3, 3, "é", "", "", 0, 0, "", 0, 3, 2, 2),
],
),
(
"aé".to_string(),
vec![
(0, 0, 0, 1, "", "a", "é", 1, 1, "a", 1, 0, 2, 2),
(1, 0, 1, 4, "a", "é", "", 1, 3, "é", 1, 1, 2, 2),
(2, 1, 1, 4, "é", "é", "", 1, 3, "", 0, 4, 2, 2),
(3, 1, 1, 4, "é", "é", "", 1, 3, "", 0, 4, 2, 2),
(4, 1, 4, 4, "é", "", "", 0, 0, "", 0, 4, 2, 2),
(5, 1, 4, 4, "é", "", "", 0, 0, "", 0, 4, 2, 2),
],
),
(
"éa".to_string(),
vec![
(0, 0, 0, 2, "", "é", "a", 1, 2, "é", 1, 0, 2, 2),
(1, 0, 0, 2, "é", "é", "a", 1, 2, "a", 1, 2, 2, 2),
(2, 0, 2, 3, "é", "a", "", 1, 1, "", 0, 3, 2, 2),
(3, 2, 3, 3, "a", "", "", 0, 0, "", 0, 3, 2, 2),
(4, 2, 3, 3, "a", "", "", 0, 0, "", 0, 3, 2, 2),
],
),
(
"éa".to_string(),
vec![
(0, 0, 0, 3, "", "é", "a", 1, 3, "é", 1, 0, 2, 2),
(1, 0, 0, 3, "é", "é", "a", 1, 3, "a", 1, 3, 2, 2),
(2, 0, 0, 3, "é", "é", "a", 1, 3, "", 0, 4, 2, 2),
(3, 0, 3, 4, "é", "a", "", 1, 1, "", 0, 4, 2, 2),
(4, 3, 4, 4, "a", "", "", 0, 0, "", 0, 4, 2, 2),
(5, 3, 4, 4, "a", "", "", 0, 0, "", 0, 4, 2, 2),
],
),
(
"abcd".to_string(),
vec![
(0, 0, 0, 1, "", "a", "b", 1, 1, "a", 1, 0, 4, 4),
(1, 0, 1, 2, "a", "b", "c", 1, 1, "b", 1, 1, 4, 4),
(2, 1, 2, 3, "b", "c", "d", 1, 1, "c", 1, 2, 4, 4),
(3, 2, 3, 4, "c", "d", "", 1, 1, "d", 1, 3, 4, 4),
(4, 3, 4, 4, "d", "", "", 0, 0, "", 0, 4, 4, 4),
(5, 3, 4, 4, "d", "", "", 0, 0, "", 0, 4, 4, 4),
],
),
(
"abcहि".to_string(),
vec![
(0, 0, 0, 1, "", "a", "b", 1, 1, "a", 1, 0, 4, 5),
(1, 0, 1, 2, "a", "b", "c", 1, 1, "b", 1, 1, 4, 5),
(2, 1, 2, 3, "b", "c", "हि", 1, 1, "c", 1, 2, 4, 5),
(3, 2, 3, 9, "c", "हि", "", 2, 6, "हि", 2, 3, 4, 5),
(4, 3, 3, 9, "हि", "हि", "", 2, 6, "", 0, 9, 4, 5),
(5, 3, 3, 9, "हि", "हि", "", 2, 6, "", 0, 9, 4, 5),
(6, 3, 3, 9, "हि", "हि", "", 2, 6, "", 0, 9, 4, 5),
(7, 3, 3, 9, "हि", "हि", "", 2, 6, "", 0, 9, 4, 5),
(8, 3, 3, 9, "हि", "हि", "", 2, 6, "", 0, 9, 4, 5),
(9, 3, 9, 9, "हि", "", "", 0, 0, "", 0, 9, 4, 5),
(10, 3, 9, 9, "हि", "", "", 0, 0, "", 0, 9, 4, 5),
],
),
(
"हिन्दीH🧑🌾e‘︀o‘︁réé".to_string(),
vec![
(0, 0, 0, 6, "", "हि", "न्दी", 2, 6, "हि", 2, 0, 12, 18),
(1, 0, 0, 6, "हि", "हि", "न्दी", 2, 6, "न्दी", 3, 6, 12, 18),
(2, 0, 0, 6, "हि", "हि", "न्दी", 2, 6, "H", 1, 18, 12, 18),
(3, 0, 0, 6, "हि", "हि", "न्दी", 2, 6, "🧑", 2, 19, 12, 18),
(4, 0, 0, 6, "हि", "हि", "न्दी", 2, 6, "🌾", 2, 23, 12, 18),
(5, 0, 0, 6, "हि", "हि", "न्दी", 2, 6, "e", 1, 27, 12, 18),
(6, 0, 6, 18, "हि", "न्दी", "H", 3, 12, "‘︀", 1, 28, 12, 18),
(7, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "o", 1, 34, 12, 18),
(8, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "‘︁", 2, 35, 12, 18),
(9, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "r", 1, 41, 12, 18),
(10, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "é", 1, 42, 12, 18),
(11, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "é", 1, 44, 12, 18),
(12, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "", 0, 47, 12, 18),
(13, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "", 0, 47, 12, 18),
(14, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "", 0, 47, 12, 18),
(15, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "", 0, 47, 12, 18),
(16, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "", 0, 47, 12, 18),
(17, 6, 6, 18, "न्दी", "न्दी", "H", 3, 12, "", 0, 47, 12, 18),
(18, 6, 18, 19, "न्दी", "H", "🧑", 1, 1, "", 0, 47, 12, 18),
(19, 18, 19, 23, "H", "🧑", "🌾", 2, 4, "", 0, 47, 12, 18),
(20, 19, 19, 23, "🧑", "🧑", "🌾", 2, 4, "", 0, 47, 12, 18),
(21, 19, 19, 23, "🧑", "🧑", "🌾", 2, 4, "", 0, 47, 12, 18),
(22, 19, 19, 23, "🧑", "🧑", "🌾", 2, 4, "", 0, 47, 12, 18),
(23, 19, 23, 27, "🧑", "🌾", "e", 2, 4, "", 0, 47, 12, 18),
(24, 23, 23, 27, "🌾", "🌾", "e", 2, 4, "", 0, 47, 12, 18),
(25, 23, 23, 27, "🌾", "🌾", "e", 2, 4, "", 0, 47, 12, 18),
(26, 23, 23, 27, "🌾", "🌾", "e", 2, 4, "", 0, 47, 12, 18),
(27, 23, 27, 28, "🌾", "e", "‘︀", 1, 1, "", 0, 47, 12, 18),
(28, 27, 28, 34, "e", "‘︀", "o", 1, 6, "", 0, 47, 12, 18),
(29, 28, 28, 34, "‘︀", "‘︀", "o", 1, 6, "", 0, 47, 12, 18),
(30, 28, 28, 34, "‘︀", "‘︀", "o", 1, 6, "", 0, 47, 12, 18),
(31, 28, 28, 34, "‘︀", "‘︀", "o", 1, 6, "", 0, 47, 12, 18),
(32, 28, 28, 34, "‘︀", "‘︀", "o", 1, 6, "", 0, 47, 12, 18),
(33, 28, 28, 34, "‘︀", "‘︀", "o", 1, 6, "", 0, 47, 12, 18),
(34, 28, 34, 35, "‘︀", "o", "‘︁", 1, 1, "", 0, 47, 12, 18),
(35, 34, 35, 41, "o", "‘︁", "r", 2, 6, "", 0, 47, 12, 18),
(36, 35, 35, 41, "‘︁", "‘︁", "r", 2, 6, "", 0, 47, 12, 18),
(37, 35, 35, 41, "‘︁", "‘︁", "r", 2, 6, "", 0, 47, 12, 18),
(38, 35, 35, 41, "‘︁", "‘︁", "r", 2, 6, "", 0, 47, 12, 18),
(39, 35, 35, 41, "‘︁", "‘︁", "r", 2, 6, "", 0, 47, 12, 18),
(40, 35, 35, 41, "‘︁", "‘︁", "r", 2, 6, "", 0, 47, 12, 18),
(41, 35, 41, 42, "‘︁", "r", "é", 1, 1, "", 0, 47, 12, 18),
(42, 41, 42, 44, "r", "é", "é", 1, 2, "", 0, 47, 12, 18),
(43, 42, 42, 44, "é", "é", "é", 1, 2, "", 0, 47, 12, 18),
(44, 42, 44, 47, "é", "é", "", 1, 3, "", 0, 47, 12, 18),
(45, 44, 44, 47, "é", "é", "", 1, 3, "", 0, 47, 12, 18),
(46, 44, 44, 47, "é", "é", "", 1, 3, "", 0, 47, 12, 18),
(47, 44, 47, 47, "é", "", "", 0, 0, "", 0, 47, 12, 18),
(48, 44, 47, 47, "é", "", "", 0, 0, "", 0, 47, 12, 18),
],
),
];
for (st, expected) in test_cases {
run_grapheme_test(&st, expected);
}
}
#[test]
fn test_num_graphemes() {
assert_eq!(num_graphemes(""), 0);
assert_eq!(num_graphemes("hello"), 5);
assert_eq!(num_graphemes("😊"), 1);
assert_eq!(num_graphemes("😊b"), 2);
assert_eq!(num_graphemes("a😊"), 2);
assert_eq!(num_graphemes("😊😊"), 2);
assert_eq!(num_graphemes("hello 😊 world"), 13);
assert_eq!(num_graphemes("é"), 1);
let complex_str = "áb̌c̃d̄";
assert_eq!(num_graphemes(complex_str), 4);
let flag_str = "🇫🇷"; assert_eq!(num_graphemes(flag_str), 1);
}
}