1#[must_use]
37pub fn split_version(s: &str) -> Vec<String> {
38 let mut parts = Vec::new();
39 let mut current = String::new();
40 let mut prev_digit: Option<bool> = None;
41 for ch in s.chars() {
42 if ch == '.' || ch == '-' {
43 if !current.is_empty() {
44 parts.push(std::mem::take(&mut current));
45 }
46 prev_digit = None;
47 } else {
48 let is_digit = ch.is_ascii_digit();
49 if let Some(was_digit) = prev_digit
50 && is_digit != was_digit
51 && !current.is_empty()
52 {
53 parts.push(std::mem::take(&mut current));
54 }
55 current.push(ch);
56 prev_digit = Some(is_digit);
57 }
58 }
59 if !current.is_empty() {
60 parts.push(current);
61 }
62 parts
63}
64
65#[must_use]
80pub fn compare_versions(a: &str, b: &str) -> i64 {
81 let pa = split_version(a);
82 let pb = split_version(b);
83 let max_len = pa.len().max(pb.len());
84 for i in 0..max_len {
85 let ca = pa.get(i).map(String::as_str).unwrap_or("");
86 let cb = pb.get(i).map(String::as_str).unwrap_or("");
87 let ord = match (ca.parse::<i64>(), cb.parse::<i64>()) {
88 (Ok(na), Ok(nb)) => na.cmp(&nb),
89 _ => match (ca, cb) {
90 ("pre", "pre") => std::cmp::Ordering::Equal,
91 ("pre", _) => std::cmp::Ordering::Less,
92 (_, "pre") => std::cmp::Ordering::Greater,
93 _ => ca.cmp(cb),
94 },
95 };
96 if ord != std::cmp::Ordering::Equal {
97 return if ord == std::cmp::Ordering::Less { -1 } else { 1 };
98 }
99 }
100 0
101}
102
103#[must_use]
125pub fn cppnix_format_float(f: f64) -> String {
126 if f.is_nan() {
127 return "NaN".to_string();
128 }
129 if f.is_infinite() {
130 return if f > 0.0 { "inf".to_string() } else { "-inf".to_string() };
131 }
132 if f == 0.0 {
133 return "0".to_string();
134 }
135
136 let exp = f.abs().log10().floor() as i32;
139 if (-4..6).contains(&exp) {
140 let after_decimal = (5 - exp).max(0) as usize;
144 let raw = format!("{f:.*}", after_decimal);
145 if let Some((whole, frac)) = raw.split_once('.') {
146 let trimmed = frac.trim_end_matches('0');
147 if trimmed.is_empty() {
148 whole.to_string()
149 } else {
150 format!("{whole}.{trimmed}")
151 }
152 } else {
153 raw
154 }
155 } else {
156 let raw = format!("{f:.5e}");
159 if let Some((mantissa, exp_part)) = raw.split_once('e') {
161 let mantissa_trimmed =
162 if let Some((w, frac)) = mantissa.split_once('.') {
163 let trimmed = frac.trim_end_matches('0');
164 if trimmed.is_empty() {
165 w.to_string()
166 } else {
167 format!("{w}.{trimmed}")
168 }
169 } else {
170 mantissa.to_string()
171 };
172 let exp_part_signed = if exp_part.starts_with('-') {
175 exp_part.to_string()
176 } else {
177 format!("+{exp_part}")
178 };
179 format!("{mantissa_trimmed}e{exp_part_signed}")
180 } else {
181 raw
182 }
183 }
184}
185
186#[must_use]
192pub fn parse_drv_name(s: &str) -> (String, String) {
193 let bytes = s.as_bytes();
194 for i in (0..bytes.len()).rev() {
195 if bytes[i] == b'-'
196 && i + 1 < bytes.len()
197 && bytes[i + 1].is_ascii_digit()
198 {
199 return (s[..i].to_string(), s[i + 1..].to_string());
200 }
201 }
202 (s.to_string(), String::new())
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use proptest::prelude::*;
209
210 #[test]
211 fn rc_orders_above_pre() {
212 assert_eq!(compare_versions("1.0-rc1", "1.0-pre1"), 1);
213 assert_eq!(compare_versions("1.0-pre1", "1.0-rc1"), -1);
214 }
215
216 #[test]
217 fn numeric_components_compare_numerically() {
218 assert_eq!(compare_versions("1.10", "1.2"), 1);
219 assert_eq!(compare_versions("1.2", "1.10"), -1);
220 assert_eq!(compare_versions("1.0", "1.0"), 0);
221 }
222
223 #[test]
224 fn missing_components_order_below_present_components() {
225 assert_eq!(compare_versions("1.0", "1.0.0"), -1);
228 assert_eq!(compare_versions("1.0.0", "1.0"), 1);
229 }
230
231 #[test]
232 fn pre_below_everything_except_pre() {
233 assert_eq!(compare_versions("1.0-pre1", "1.0-pre1"), 0);
234 assert_eq!(compare_versions("1.0-pre1", "1.0"), -1);
235 assert_eq!(compare_versions("1.0", "1.0-pre1"), 1);
236 assert_eq!(compare_versions("1.0-pre", "1.0-alpha"), -1);
238 assert_eq!(compare_versions("1.0-pre", "1.0-beta"), -1);
239 assert_eq!(compare_versions("1.0-pre", "1.0-rc"), -1);
240 }
241
242 #[test]
243 fn split_version_basic_shapes() {
244 assert_eq!(split_version("1.0-rc1"), vec!["1", "0", "rc", "1"]);
245 assert_eq!(split_version("1.0.0-pre"), vec!["1", "0", "0", "pre"]);
246 assert_eq!(split_version("2024a"), vec!["2024", "a"]);
247 }
248
249 #[test]
250 fn cppnix_format_float_known_outputs() {
251 assert_eq!(cppnix_format_float(1.0 / 3.0), "0.333333");
253 assert_eq!(cppnix_format_float(3.14159), "3.14159");
254 assert_eq!(cppnix_format_float(1.5), "1.5");
255 assert_eq!(cppnix_format_float(3.0), "3");
256 assert_eq!(cppnix_format_float(0.0), "0");
257 assert_eq!(cppnix_format_float(-3.14), "-3.14");
258 assert_eq!(cppnix_format_float(-3.0), "-3");
259 }
260
261 #[test]
262 fn cppnix_format_float_nan_and_infinity() {
263 assert_eq!(cppnix_format_float(f64::NAN), "NaN");
264 assert_eq!(cppnix_format_float(f64::INFINITY), "inf");
265 assert_eq!(cppnix_format_float(f64::NEG_INFINITY), "-inf");
266 }
267
268 #[test]
269 fn parse_drv_name_recovers_split() {
270 let (n, v) = parse_drv_name("hello-1.2.3");
271 assert_eq!(n, "hello");
272 assert_eq!(v, "1.2.3");
273 let (n, v) = parse_drv_name("nix-darwin-config");
274 assert_eq!(n, "nix-darwin-config");
275 assert_eq!(v, "");
276 }
277
278 proptest! {
279 #[test]
281 fn compare_versions_antisymmetric(
282 a in "[0-9a-z.-]{1,20}",
283 b in "[0-9a-z.-]{1,20}",
284 ) {
285 let ab = compare_versions(&a, &b);
286 let ba = compare_versions(&b, &a);
287 prop_assert_eq!(ab, -ba);
288 }
289
290 #[test]
292 fn compare_versions_reflexive(a in "[0-9a-z.-]{1,20}") {
293 prop_assert_eq!(compare_versions(&a, &a), 0);
294 }
295 }
296}