1#![no_std]
28#![forbid(unsafe_code)]
29#![doc(html_root_url = "https://docs.rs/decamelize/0.1.0")]
30
31extern crate alloc;
32
33use alloc::string::String;
34use alloc::vec::Vec;
35
36#[cfg(doctest)]
38#[doc = include_str!("../README.md")]
39struct ReadmeDoctests;
40
41#[must_use]
50pub fn decamelize(text: &str) -> String {
51 decamelize_with(text, "_", false)
52}
53
54#[must_use]
65pub fn decamelize_with(
66 text: &str,
67 separator: &str,
68 preserve_consecutive_uppercase: bool,
69) -> String {
70 if text.encode_utf16().take(2).count() < 2 {
72 return if preserve_consecutive_uppercase {
73 text.into()
74 } else {
75 text.to_lowercase()
76 };
77 }
78
79 let split = split_lower_then_upper(text, separator);
81
82 if preserve_consecutive_uppercase {
83 let lowered = lowercase_isolated(&split);
84 split_upper_run(&lowered, separator, true)
85 } else {
86 split_upper_run(&split, separator, false).to_lowercase()
87 }
88}
89
90fn is_upper(c: char) -> bool {
91 c.is_uppercase()
92}
93
94fn is_lower(c: char) -> bool {
95 c.is_lowercase()
96}
97
98fn is_digit(c: char) -> bool {
100 c.is_ascii_digit()
101}
102
103fn is_upper_or_digit(c: char) -> bool {
104 is_upper(c) || is_digit(c)
105}
106
107fn split_lower_then_upper(text: &str, separator: &str) -> String {
110 let mut out = String::with_capacity(text.len());
111 let mut prev: Option<char> = None;
112 for c in text.chars() {
113 if let Some(p) = prev {
114 if (is_lower(p) || is_digit(p)) && is_upper(c) {
115 out.push_str(separator);
116 }
117 }
118 out.push(c);
119 prev = Some(c);
120 }
121 out
122}
123
124fn lowercase_isolated(text: &str) -> String {
127 let chars: Vec<char> = text.chars().collect();
128 let mut out = String::with_capacity(text.len());
129 for (i, &c) in chars.iter().enumerate() {
130 let isolated = is_upper_or_digit(c)
131 && (i == 0 || !is_upper_or_digit(chars[i - 1]))
132 && (i + 1 >= chars.len() || !is_upper_or_digit(chars[i + 1]));
133 if isolated {
134 out.extend(c.to_lowercase());
135 } else {
136 out.push(c);
137 }
138 }
139 out
140}
141
142fn split_upper_run(text: &str, separator: &str, lowercase_moved: bool) -> String {
150 let chars: Vec<char> = text.chars().collect();
151 let n = chars.len();
152 let mut out = String::with_capacity(text.len() + separator.len());
153 let mut i = 0;
154 while i < n {
155 if is_upper(chars[i]) && (i == 0 || !is_upper(chars[i - 1])) {
156 let mut j = i;
158 while j < n && is_upper(chars[j]) {
159 j += 1;
160 }
161 let run_len = j - i;
162 if run_len >= 2 && j < n && is_lower(chars[j]) {
163 for &c in &chars[i..j - 1] {
164 out.push(c);
165 }
166 out.push_str(separator);
167 if lowercase_moved {
168 out.extend(chars[j - 1].to_lowercase());
169 } else {
170 out.push(chars[j - 1]);
171 }
172 } else {
173 for &c in &chars[i..j] {
174 out.push(c);
175 }
176 }
177 i = j;
178 } else {
179 out.push(chars[i]);
180 i += 1;
181 }
182 }
183 out
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn basic() {
192 assert_eq!(decamelize("unicornsAndRainbows"), "unicorns_and_rainbows");
193 assert_eq!(decamelize("fooBar"), "foo_bar");
194 assert_eq!(decamelize("p2pNetwork"), "p2p_network");
195 }
196
197 #[test]
198 fn acronyms() {
199 assert_eq!(decamelize("XMLHttpRequest"), "xml_http_request");
200 assert_eq!(decamelize("testGUILabel"), "test_gui_label");
201 assert_eq!(decamelize("testID"), "test_id");
202 assert_eq!(decamelize("ABCdef"), "ab_cdef");
203 assert_eq!(decamelize("fooBAR"), "foo_bar");
204 }
205
206 #[test]
207 fn digits_and_short() {
208 assert_eq!(decamelize("foo2bar"), "foo2bar");
209 assert_eq!(decamelize("test123Number"), "test123_number");
210 assert_eq!(decamelize("ID"), "id");
211 assert_eq!(decamelize("a"), "a");
212 assert_eq!(decamelize("A"), "a");
213 assert_eq!(decamelize(""), "");
214 assert_eq!(decamelize("__foo__bar__"), "__foo__bar__");
215 }
216
217 #[test]
218 fn separators() {
219 assert_eq!(decamelize_with("fooBar", "-", false), "foo-bar");
220 assert_eq!(
221 decamelize_with("unicornsAndRainbows", " ", false),
222 "unicorns and rainbows"
223 );
224 assert_eq!(
225 decamelize_with("XMLHttpRequest", "-", false),
226 "xml-http-request"
227 );
228 }
229
230 #[test]
231 fn preserve_consecutive_uppercase() {
232 assert_eq!(decamelize_with("testGUILabel", "_", true), "test_GUI_label");
233 assert_eq!(
234 decamelize_with("XMLHttpRequest", "_", true),
235 "XML_http_request"
236 );
237 assert_eq!(decamelize_with("testID", "_", true), "test_ID");
238 assert_eq!(decamelize_with("fooBAR", "_", true), "foo_BAR");
239 assert_eq!(decamelize_with("ABCdef", "_", true), "AB_cdef");
240 assert_eq!(decamelize_with("A", "_", true), "A");
241 }
242}