1pub mod budget;
15
16pub mod chunk;
18
19pub use chunk::{ChunkOptions, chunk_text};
20
21fn char_cpt(ch: char) -> f32 {
24 let cp = ch as u32;
25 match cp {
26 0x1F600..=0x1F64F | 0x1F300..=0x1F5FF | 0x1F680..=0x1F6FF | 0x2600..=0x26FF => 1.0,
28 0x3040..=0x30FF | 0x4E00..=0x9FFF | 0xAC00..=0xD7AF => 1.5,
30 0x0600..=0x06FF | 0x0900..=0x097F | 0x0E00..=0x0E7F => 2.0,
32 0x0400..=0x04FF => 2.0,
34 0x0370..=0x03FF => 2.0,
36 0x0590..=0x05FF => 2.0,
38 _ => DEFAULT_CPT,
39 }
40}
41
42const DEFAULT_CPT: f32 = 4.0;
44
45const WS_WEIGHT: f32 = 0.25;
47
48pub fn estimate_tokens(text: &str) -> usize {
52 if text.is_empty() {
53 return 0;
54 }
55
56 let mut total_weight: f32 = 0.0;
57
58 for ch in text.chars() {
59 if ch.is_ascii_control() {
60 continue;
61 }
62 if ch.is_whitespace() {
63 total_weight += WS_WEIGHT;
64 continue;
65 }
66 total_weight += 1.0 / char_cpt(ch);
67 }
68
69 if total_weight == 0.0 {
70 return 0;
71 }
72
73 total_weight.round() as usize
74}
75
76pub fn estimate_tokens_min(text: &str, min: usize) -> usize {
78 estimate_tokens(text).max(min)
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 #[test]
86 fn empty_string() {
87 assert_eq!(estimate_tokens(""), 0);
88 }
89
90 #[test]
91 fn ascii_text() {
92 let tokens = estimate_tokens("Hello, world! This is a test.");
93 assert!(tokens > 3 && tokens < 15, "got {tokens}");
95 }
96
97 #[test]
98 fn cjk_text() {
99 let tokens = estimate_tokens("こんにちは世界");
100 assert!(tokens > 2 && tokens < 10, "got {tokens}");
102 }
103
104 #[test]
105 fn mixed_scripts() {
106 let tokens = estimate_tokens("Hello こんにちは مرحبا");
107 assert!(tokens > 0);
108 }
109
110 #[test]
111 fn emoji() {
112 let tokens = estimate_tokens("🎉🚀👍");
113 assert!(tokens >= 2, "got {tokens}");
114 }
115
116 #[test]
117 fn min_clamp() {
118 assert_eq!(estimate_tokens_min("", 5), 5);
119 }
120
121 #[test]
122 fn long_text_proportional() {
123 let short = estimate_tokens("Hello world");
124 let long = estimate_tokens("Hello world Hello world Hello world");
125 assert!(long > short, "long={long} should be > short={short}");
126 }
127
128 #[test]
129 fn cyrillic_text() {
130 let tokens = estimate_tokens("Привет мир");
131 assert!(tokens > 2 && tokens < 10, "got {tokens}");
133 }
134
135 #[test]
136 fn greek_text() {
137 let tokens = estimate_tokens("Γεια σου κόσμε");
138 assert!(tokens > 0 && tokens < 10, "got {tokens}");
139 }
140
141 #[test]
142 fn hebrew_text() {
143 let tokens = estimate_tokens("שלום עולם");
144 assert!(tokens > 0 && tokens < 10, "got {tokens}");
145 }
146
147 #[test]
148 fn whitespace_contributes_tokens() {
149 let no_space = estimate_tokens("abcdef");
150 let with_space = estimate_tokens("a b c d e f");
151 assert!(
153 with_space > no_space / 2,
154 "with_space={with_space} should not be negligible vs no_space={no_space}"
155 );
156 }
157}