#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default)]
pub struct U16Index(usize);
impl U16Index {
pub const ZERO: U16Index = U16Index(0);
pub fn new(i: usize) -> Self {
U16Index(i)
}
pub fn get(self) -> usize {
self.0
}
}
pub fn len(s: &str) -> usize {
s.chars().map(char::len_utf16).sum()
}
pub struct Units(Vec<u16>);
impl Units {
pub fn of(s: &str) -> Self {
Units(s.encode_utf16().collect())
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn as_slice(&self) -> &[u16] {
&self.0
}
pub fn unit(&self, i: usize) -> Option<u16> {
self.0.get(i).copied()
}
pub fn code_point(&self, i: usize) -> Option<u32> {
let hi = self.unit(i)? as u32;
if (0xD800..0xDC00).contains(&hi) {
if let Some(lo) = self.unit(i + 1).map(u32::from) {
if (0xDC00..0xE000).contains(&lo) {
return Some(0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00));
}
}
}
Some(hi)
}
pub fn slice(&self, lo: usize, hi: usize) -> String {
let lo = lo.min(self.0.len());
let hi = hi.clamp(lo, self.0.len());
to_string_lossy(&self.0[lo..hi])
}
pub fn unit_str(&self, i: usize) -> Option<String> {
self.unit(i).map(|u| to_string_lossy(&[u]))
}
}
pub fn to_string_lossy(units: &[u16]) -> String {
String::from_utf16_lossy(units)
}
pub fn to_uint16(n: f64) -> u16 {
if !n.is_finite() {
return 0;
}
(n.trunc().rem_euclid(65536.0)) as u16
}
pub fn cmp_units(a: &str, b: &str) -> std::cmp::Ordering {
a.encode_utf16().cmp(b.encode_utf16())
}
pub fn index_of_byte(s: &str, byte: usize) -> U16Index {
let byte = byte.min(s.len());
let mut b = byte;
while b > 0 && !s.is_char_boundary(b) {
b -= 1;
}
U16Index(len(&s[..b]))
}
pub fn byte_of_index(s: &str, idx: U16Index) -> usize {
let target = idx.get();
let mut units = 0usize;
for (b, c) in s.char_indices() {
if units + c.len_utf16() > target {
return b;
}
units += c.len_utf16();
}
s.len()
}
pub fn is_js_whitespace(c: char) -> bool {
matches!(
c,
'\u{9}' | '\u{B}' | '\u{C}' | '\u{20}' | '\u{A0}' | '\u{FEFF}'
| '\u{1680}' | '\u{2000}'
..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}'
| '\u{A}' | '\u{D}' | '\u{2028}' | '\u{2029}'
)
}
pub fn js_trim(s: &str) -> &str {
s.trim_matches(is_js_whitespace)
}
pub fn js_trim_start(s: &str) -> &str {
s.trim_start_matches(is_js_whitespace)
}
pub fn js_trim_end(s: &str) -> &str {
s.trim_end_matches(is_js_whitespace)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn astral_lengths_and_units() {
assert_eq!(len("𝒳"), 2);
assert_eq!(len("ab𝒳cd"), 6);
assert_eq!(len("😀🎉"), 4);
assert_eq!(len("abc"), 3);
let u = Units::of("𝒳");
assert_eq!(u.len(), 2);
assert_eq!(u.unit(0), Some(55349));
assert_eq!(u.unit(1), Some(56499));
assert_eq!(u.code_point(0), Some(119987));
assert_eq!(u.code_point(1), Some(56499));
assert_eq!(u.unit(2), None);
}
#[test]
fn slicing_a_pair_in_half_keeps_the_unit_count() {
let u = Units::of("𝒳");
assert_eq!(len(&u.slice(0, 1)), 1);
assert_eq!(len(&u.slice(1, 2)), 1);
assert_eq!(u.slice(0, 2), "𝒳");
assert_eq!(u.slice(3, 9), "");
}
#[test]
fn byte_and_index_round_trip() {
let s = "ab𝒳cd";
assert_eq!(index_of_byte(s, 6), U16Index::new(4));
assert_eq!(byte_of_index(s, U16Index::new(4)), 6);
assert_eq!(index_of_byte(s, 0), U16Index::ZERO);
assert_eq!(byte_of_index(s, U16Index::new(0)), 0);
assert_eq!(byte_of_index(s, U16Index::new(99)), s.len());
assert_eq!(byte_of_index(s, U16Index::new(3)), 2);
}
#[test]
fn relational_order_is_by_code_unit() {
use std::cmp::Ordering;
assert_eq!(cmp_units("𝒳", "\u{FFFF}"), Ordering::Less);
assert_eq!("𝒳".cmp("\u{FFFF}"), Ordering::Greater);
assert_eq!(cmp_units("𝒳", "\u{E000}"), Ordering::Less);
assert_eq!(cmp_units("\u{10FFFF}", "\u{E000}"), Ordering::Less);
assert_eq!(cmp_units("a", "b"), Ordering::Less);
assert_eq!(cmp_units("café", "cafz"), Ordering::Greater);
assert_eq!(cmp_units("café", "cagz"), Ordering::Less);
assert_eq!(cmp_units("ab", "ab"), Ordering::Equal);
assert_eq!(cmp_units("ab", "abc"), Ordering::Less);
assert_eq!(cmp_units("", "a"), Ordering::Less);
}
#[test]
fn index_of_byte_tolerates_a_non_boundary_offset() {
let s = "ab𝒳cd";
for b in 2..=5 {
assert_eq!(index_of_byte(s, b), U16Index::new(2), "byte {b}");
}
}
}