pub const OBJ_PREFIX: &str = "obj_";
pub fn strip_obj_prefix(name: &str) -> &str {
name.strip_prefix(OBJ_PREFIX).unwrap_or(name)
}
pub fn has_obj_prefix(name: &str) -> bool {
name.starts_with(OBJ_PREFIX)
}
pub fn type_eq(a: &str, b: &str) -> bool {
a == b
}
pub fn type_eq_loose(a: &str, b: &str) -> bool {
strip_obj_prefix(a).eq_ignore_ascii_case(strip_obj_prefix(b))
}
pub fn compact_version(dotted: &str) -> String {
dotted.chars().filter(|c| *c != '.').collect()
}
pub fn dotted_version(compact: &str) -> String {
let mut out = String::with_capacity(compact.len() * 2);
for (i, c) in compact.chars().enumerate() {
if i > 0 {
out.push('.');
}
out.push(c);
}
out
}
pub fn version_eq(a: &str, b: &str) -> bool {
compact_version(a) == compact_version(b)
}
pub fn split_prerelease(version: &str) -> (&str, Option<&str>) {
let bare = version.trim();
let cut = bare.trim_start_matches(|c: char| !c.is_ascii_digit());
let trimmed = if cut.contains('.') { cut } else { bare };
match trimmed.find(|c: char| !c.is_ascii_digit() && c != '.') {
None => (trimmed, None),
Some(at) => {
let release = trimmed[..at].trim_end_matches('.');
let pre = trimmed[at..].trim_start_matches(['_', '-', ' ', '.']);
(release, (!pre.is_empty()).then_some(pre))
}
}
}
pub fn release_of(version: &str) -> &str {
split_prerelease(version).0
}
pub fn is_prerelease(version: &str) -> bool {
split_prerelease(version).1.is_some()
}
pub fn prerelease_eq(a: &str, b: &str) -> bool {
let norm = |s: &str| -> String {
s.chars()
.filter(char::is_ascii_alphanumeric)
.map(|c| c.to_ascii_lowercase())
.collect()
};
norm(a) == norm(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_only_the_real_prefix() {
assert_eq!(
strip_obj_prefix("obj_EpcExternalPartReference"),
"EpcExternalPartReference"
);
assert_eq!(strip_obj_prefix("object_Thing"), "object_Thing");
assert_eq!(strip_obj_prefix("obj_"), "");
}
#[test]
fn loose_comparison_ignores_case_and_prefix() {
assert!(type_eq_loose(
"obj_TriangulatedSetRepresentation",
"triangulatedsetrepresentation"
));
assert!(!type_eq("obj_Activity", "Activity"));
assert!(type_eq("obj_Activity", "obj_Activity"));
}
#[test]
fn versions_convert_both_ways() {
assert_eq!(compact_version("2.0"), "20");
assert_eq!(compact_version("2.0.1"), "201");
assert_eq!(dotted_version("20"), "2.0");
assert_eq!(dotted_version("201"), "2.0.1");
assert!(version_eq("2.2", "22"));
assert!(!version_eq("2.2", "20"));
}
#[test]
fn a_release_has_no_prerelease_and_is_returned_whole() {
for release in ["2.0", "2.1", "2.2", "2.3", "2.0.1"] {
assert_eq!(split_prerelease(release), (release, None));
assert_eq!(release_of(release), release);
assert!(!is_prerelease(release));
}
}
#[test]
fn a_development_drop_splits_however_it_is_spelled() {
for spelling in ["2.2_dev3", "2.2dev3", "2.2-dev3", "2.2 dev3"] {
assert_eq!(
split_prerelease(spelling),
(
"2.2",
Some(spelling.trim_start_matches(['2', '.', '_', '-', ' ']))
)
);
assert_eq!(release_of(spelling), "2.2");
assert!(is_prerelease(spelling));
}
}
#[test]
fn leading_junk_is_trimmed_like_a_declared_version() {
assert_eq!(split_prerelease("Resqml V2.2_dev3"), ("2.2", Some("dev3")));
assert_eq!(release_of("V2.0"), "2.0");
}
#[test]
fn drops_are_compared_by_what_they_name_not_by_spelling() {
assert!(prerelease_eq("dev3", "DEV3"));
assert!(prerelease_eq("dev3", "dev-3"));
assert!(!prerelease_eq("dev3", "dev2"));
assert!(!prerelease_eq("dev3", "rc3"));
}
#[test]
fn a_version_that_is_only_a_suffix_claims_no_release() {
assert_eq!(split_prerelease("dev3"), ("", Some("dev3")));
assert_eq!(release_of("dev3"), "");
}
}