oxirs_ttl/toolkit/
string_interner.rs1use std::borrow::Cow;
7use std::collections::HashMap;
8use std::hash::{Hash, Hasher};
9use std::sync::Arc;
10
11#[derive(Debug, Clone)]
34pub struct StringInterner {
35 map: HashMap<InternedString, Arc<String>>,
37 stats: InternerStats,
39}
40
41#[derive(Debug, Clone, Default)]
43pub struct InternerStats {
44 pub total_requests: usize,
46 pub cache_hits: usize,
48 pub cache_misses: usize,
50 pub unique_strings: usize,
52 pub bytes_saved: usize,
54}
55
56#[derive(Debug, Clone, Eq)]
58struct InternedString(Arc<String>);
59
60impl InternedString {
61 fn new(s: Arc<String>) -> Self {
62 Self(s)
63 }
64
65 fn as_str(&self) -> &str {
66 &self.0
67 }
68}
69
70impl PartialEq for InternedString {
71 fn eq(&self, other: &Self) -> bool {
72 self.0.as_str() == other.0.as_str()
73 }
74}
75
76impl Hash for InternedString {
77 fn hash<H: Hasher>(&self, state: &mut H) {
78 self.0.as_str().hash(state);
79 }
80}
81
82impl std::borrow::Borrow<str> for InternedString {
83 fn borrow(&self) -> &str {
84 self.as_str()
85 }
86}
87
88impl StringInterner {
89 pub fn new() -> Self {
91 Self::with_capacity(1024)
92 }
93
94 pub fn with_capacity(capacity: usize) -> Self {
96 Self {
97 map: HashMap::with_capacity(capacity),
98 stats: InternerStats::default(),
99 }
100 }
101
102 pub fn with_common_namespaces() -> Self {
104 let mut interner = Self::with_capacity(2048);
105
106 let common_namespaces = [
108 "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
109 "http://www.w3.org/2000/01/rdf-schema#",
110 "http://www.w3.org/2001/XMLSchema#",
111 "http://www.w3.org/2002/07/owl#",
112 "http://xmlns.com/foaf/0.1/",
113 "http://purl.org/dc/elements/1.1/",
114 "http://purl.org/dc/terms/",
115 "http://schema.org/",
116 "http://www.w3.org/ns/shacl#",
117 "http://www.w3.org/2004/02/skos/core#",
118 "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
120 "http://www.w3.org/2000/01/rdf-schema#label",
121 "http://www.w3.org/2000/01/rdf-schema#comment",
122 "http://www.w3.org/2000/01/rdf-schema#subClassOf",
123 "http://www.w3.org/2000/01/rdf-schema#subPropertyOf",
124 "http://www.w3.org/2000/01/rdf-schema#domain",
125 "http://www.w3.org/2000/01/rdf-schema#range",
126 "http://www.w3.org/2002/07/owl#sameAs",
127 "http://www.w3.org/2002/07/owl#equivalentClass",
128 "http://www.w3.org/2002/07/owl#equivalentProperty",
129 ];
130
131 for ns in &common_namespaces {
132 interner.intern(ns);
133 }
134
135 interner.stats = InternerStats {
137 unique_strings: interner.map.len(),
138 ..Default::default()
139 };
140
141 interner
142 }
143
144 pub fn intern(&mut self, s: &str) -> Arc<String> {
149 self.stats.total_requests += 1;
150
151 if let Some(interned_key) = self.map.get_key_value(s) {
153 self.stats.cache_hits += 1;
154 self.stats.bytes_saved += s.len();
156 return interned_key.0 .0.clone();
157 }
158
159 self.stats.cache_misses += 1;
161 self.stats.unique_strings += 1;
162
163 let arc_string = Arc::new(s.to_string());
164 let key = InternedString::new(arc_string.clone());
165 self.map.insert(key, arc_string.clone());
166
167 arc_string
168 }
169
170 pub fn intern_if_beneficial<'a>(&mut self, s: &'a str, min_length: usize) -> Cow<'a, str> {
177 if s.contains("www.w3.org")
179 || s.contains("schema.org")
180 || s.contains("xmlns.com")
181 || s.contains("purl.org")
182 || matches!(s, "en" | "de" | "fr" | "es" | "ja" | "zh" | "ar" | "hi")
183 {
184 return Cow::Owned((*self.intern(s)).clone());
185 }
186
187 if s.len() >= min_length {
189 Cow::Owned((*self.intern(s)).clone())
190 } else {
191 Cow::Borrowed(s)
192 }
193 }
194
195 pub fn len(&self) -> usize {
197 self.map.len()
198 }
199
200 pub fn is_empty(&self) -> bool {
202 self.map.is_empty()
203 }
204
205 pub fn stats(&self) -> &InternerStats {
207 &self.stats
208 }
209
210 pub fn hit_rate(&self) -> f64 {
212 if self.stats.total_requests == 0 {
213 return 0.0;
214 }
215 self.stats.cache_hits as f64 / self.stats.total_requests as f64
216 }
217
218 pub fn clear(&mut self) {
220 self.map.clear();
221 self.stats = InternerStats::default();
222 }
223
224 pub fn memory_usage(&self) -> usize {
226 const HASHMAP_ENTRY_OVERHEAD: usize = 24; const ARC_OVERHEAD: usize = 16; let mut total = 0;
231
232 total += self.map.capacity() * HASHMAP_ENTRY_OVERHEAD;
234
235 for key in self.map.keys() {
237 total += ARC_OVERHEAD; total += ARC_OVERHEAD; total += ARC_OVERHEAD; total += key.as_str().len(); total += std::mem::size_of::<String>(); }
243
244 total
245 }
246
247 pub fn shrink_to_fit(&mut self) {
249 self.map.shrink_to_fit();
250 }
251}
252
253impl Default for StringInterner {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259impl InternerStats {
260 pub fn report(&self) -> String {
262 let hit_rate = if self.total_requests > 0 {
263 (self.cache_hits as f64 / self.total_requests as f64) * 100.0
264 } else {
265 0.0
266 };
267
268 format!(
269 "String Interning Statistics:\n\
270 - Total requests: {}\n\
271 - Cache hits: {} ({:.1}%)\n\
272 - Cache misses: {}\n\
273 - Unique strings: {}\n\
274 - Bytes saved: {} ({:.1} KB)",
275 self.total_requests,
276 self.cache_hits,
277 hit_rate,
278 self.cache_misses,
279 self.unique_strings,
280 self.bytes_saved,
281 self.bytes_saved as f64 / 1024.0
282 )
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_basic_interning() {
292 let mut interner = StringInterner::new();
293
294 let s1 = interner.intern("http://example.org/");
295 let s2 = interner.intern("http://example.org/");
296
297 assert!(Arc::ptr_eq(&s1, &s2));
299 assert_eq!(interner.len(), 1);
300 }
301
302 #[test]
303 fn test_different_strings() {
304 let mut interner = StringInterner::new();
305
306 let s1 = interner.intern("http://example.org/a");
307 let s2 = interner.intern("http://example.org/b");
308
309 assert!(!Arc::ptr_eq(&s1, &s2));
311 assert_eq!(interner.len(), 2);
312 }
313
314 #[test]
315 fn test_stats() {
316 let mut interner = StringInterner::new();
317
318 interner.intern("test");
319 interner.intern("test");
320 interner.intern("test");
321 interner.intern("other");
322
323 let stats = interner.stats();
324 assert_eq!(stats.total_requests, 4);
325 assert_eq!(stats.cache_hits, 2); assert_eq!(stats.cache_misses, 2); assert_eq!(stats.unique_strings, 2);
328 }
329
330 #[test]
331 fn test_hit_rate() {
332 let mut interner = StringInterner::new();
333
334 interner.intern("a");
335 interner.intern("a");
336 interner.intern("b");
337 interner.intern("b");
338
339 assert_eq!(interner.hit_rate(), 0.5);
341 }
342
343 #[test]
344 fn test_common_namespaces() {
345 let interner = StringInterner::with_common_namespaces();
346
347 assert!(interner.len() > 10);
349
350 assert_eq!(interner.stats().total_requests, 0);
352 assert_eq!(interner.stats().cache_hits, 0);
353 }
354
355 #[test]
356 fn test_intern_if_beneficial() {
357 let mut interner = StringInterner::new();
358
359 let result = interner.intern_if_beneficial("ab", 10);
361 assert!(matches!(result, Cow::Borrowed(_)));
362
363 let result = interner.intern_if_beneficial("http://example.org/very/long/uri", 10);
365 assert!(matches!(result, Cow::Owned(_)));
366
367 let result = interner.intern_if_beneficial("www.w3.org", 100);
369 assert!(matches!(result, Cow::Owned(_)));
370 }
371
372 #[test]
373 fn test_memory_usage() {
374 let mut interner = StringInterner::new();
375
376 interner.intern("short");
377 interner.intern("a much longer string that takes more memory");
378
379 let usage = interner.memory_usage();
380 assert!(usage > 0);
381 println!("Memory usage: {} bytes", usage);
382 }
383
384 #[test]
385 fn test_clear() {
386 let mut interner = StringInterner::new();
387
388 interner.intern("test1");
389 interner.intern("test2");
390 assert_eq!(interner.len(), 2);
391
392 interner.clear();
393 assert_eq!(interner.len(), 0);
394 assert_eq!(interner.stats().total_requests, 0);
395 }
396
397 #[test]
398 fn test_rdf_use_case() {
399 let mut interner = StringInterner::with_common_namespaces();
400
401 let rdf_type = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
403
404 let initial_unique = interner.len();
405
406 for _ in 0..1000 {
408 interner.intern(rdf_type);
409 }
410
411 assert_eq!(interner.len(), initial_unique);
413
414 assert!(interner.hit_rate() > 0.95);
416
417 println!("{}", interner.stats().report());
418 }
419
420 #[test]
421 fn test_stats_report() {
422 let mut interner = StringInterner::new();
423
424 interner.intern("test");
425 interner.intern("test");
426 interner.intern("other");
427
428 let report = interner.stats().report();
429 assert!(report.contains("Total requests: 3"));
430 assert!(report.contains("Cache hits: 1"));
431 assert!(report.contains("Unique strings: 2"));
432 }
433}