#[cfg(feature = "width-cache")]
use ahash::AHasher;
#[cfg(feature = "width-cache")]
use lru::LruCache;
#[cfg(feature = "width-cache")]
use std::cell::RefCell;
#[cfg(feature = "width-cache")]
use std::hash::{Hash, Hasher};
#[cfg(feature = "width-cache")]
use std::num::NonZeroUsize;
use unicode_width::UnicodeWidthStr;
#[cfg(feature = "width-cache")]
const DEFAULT_CACHE_SIZE: usize = 1024;
#[cfg(feature = "width-cache")]
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: usize,
pub misses: usize,
pub evictions: usize,
}
#[cfg(feature = "width-cache")]
impl CacheStats {
#[must_use]
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
#[allow(clippy::cast_precision_loss)]
{
(self.hits as f64 / total as f64) * 100.0
}
}
}
pub fn reset(&mut self) {
self.hits = 0;
self.misses = 0;
self.evictions = 0;
}
}
#[cfg(feature = "width-cache")]
struct WidthCache {
cache: LruCache<u64, usize>,
stats: CacheStats,
}
#[cfg(feature = "width-cache")]
impl WidthCache {
fn new(capacity: usize) -> Self {
Self {
cache: LruCache::new(
NonZeroUsize::new(capacity).expect("Cache capacity must be non-zero"),
),
stats: CacheStats::default(),
}
}
fn get(&mut self, s: &str) -> Option<usize> {
let hash = Self::hash_string(s);
if let Some(&width) = self.cache.get(&hash) {
self.stats.hits += 1;
Some(width)
} else {
self.stats.misses += 1;
None
}
}
fn insert(&mut self, s: &str, width: usize) {
let hash = Self::hash_string(s);
if self.cache.put(hash, width).is_some() {
self.stats.evictions += 1;
}
}
fn hash_string(s: &str) -> u64 {
let mut hasher = AHasher::default();
s.hash(&mut hasher);
hasher.finish()
}
fn stats(&self) -> CacheStats {
self.stats.clone()
}
fn clear(&mut self) {
self.cache.clear();
self.stats.reset();
}
}
#[cfg(feature = "width-cache")]
thread_local! {
static WIDTH_CACHE: RefCell<WidthCache> = RefCell::new(WidthCache::new(DEFAULT_CACHE_SIZE));
}
#[cfg(feature = "width-cache")]
#[must_use]
pub fn cached_unicode_width(s: &str) -> usize {
WIDTH_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(width) = cache.get(s) {
width
} else {
let width = s.width();
cache.insert(s, width);
width
}
})
}
#[cfg(not(feature = "width-cache"))]
#[must_use]
pub fn cached_unicode_width(s: &str) -> usize {
s.width()
}
#[cfg(feature = "width-cache")]
#[must_use]
pub fn cache_stats() -> CacheStats {
WIDTH_CACHE.with(|cache| cache.borrow().stats())
}
#[cfg(feature = "width-cache")]
pub fn clear_cache() {
WIDTH_CACHE.with(|cache| cache.borrow_mut().clear());
}
#[cfg(feature = "width-cache")]
pub fn set_cache_size(size: usize) {
WIDTH_CACHE.with(|cache| {
*cache.borrow_mut() = WidthCache::new(size);
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_width_calculation() {
let width = cached_unicode_width("Hello");
assert_eq!(width, 5);
}
#[test]
fn test_unicode_width() {
let width = cached_unicode_width("你好");
assert_eq!(width, 4); }
#[test]
fn test_emoji_width() {
let width = cached_unicode_width("🌍");
assert!(width > 0);
}
#[cfg(feature = "width-cache")]
#[test]
fn test_cache_hit() {
clear_cache();
let _ = cached_unicode_width("test");
let stats1 = cache_stats();
assert_eq!(stats1.misses, 1);
assert_eq!(stats1.hits, 0);
let _ = cached_unicode_width("test");
let stats2 = cache_stats();
assert_eq!(stats2.hits, 1);
assert_eq!(stats2.misses, 1);
}
#[cfg(feature = "width-cache")]
#[test]
fn test_cache_clear() {
clear_cache();
let _ = cached_unicode_width("test");
clear_cache();
let stats = cache_stats();
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[cfg(feature = "width-cache")]
#[test]
fn test_cache_stats() {
clear_cache();
for _ in 0..10 {
let _ = cached_unicode_width("test");
}
let stats = cache_stats();
assert_eq!(stats.hits, 9);
assert_eq!(stats.misses, 1);
assert!(stats.hit_rate() > 80.0);
}
#[cfg(feature = "width-cache")]
#[test]
fn test_cache_eviction() {
set_cache_size(2);
clear_cache();
let _ = cached_unicode_width("a");
let _ = cached_unicode_width("b");
let _ = cached_unicode_width("c");
let _ = cached_unicode_width("d");
let stats = cache_stats();
assert!(
stats.misses >= 4,
"Expected 4 initial misses, got {}",
stats.misses
);
}
#[cfg(feature = "width-cache")]
#[test]
fn test_set_cache_size() {
set_cache_size(512);
clear_cache();
for i in 0..100 {
let _ = cached_unicode_width(&format!("test{}", i));
}
let stats = cache_stats();
assert_eq!(stats.misses, 100);
assert_eq!(stats.evictions, 0); }
}