1use crate::normalize_version::{bound_every_version, interval_to_requirement};
2
3pub fn words_to_operators(range: &str) -> String {
16 let range = strip_prefix_ignore_case(range, "all versions").unwrap_or(range);
17 let range = strip_prefix_ignore_case(range, "from ").unwrap_or(range);
18
19 let upper_bound_prefixes = [
20 ("up to and including", "<="),
21 ("up to", "<="),
22 ("prior to", "<"),
23 ("earlier than", "<"),
24 ("before", "<"),
25 ("until", "<"),
26 ];
27 for (phrase, operator) in upper_bound_prefixes {
28 if let Some(versions) = strip_prefix_ignore_case(range, phrase) {
29 return bound_every_version(versions, operator, "");
30 }
31 }
32
33 let bound_suffixes = [
34 ("and newer", ">="),
35 ("or newer", ">="),
36 ("and later", ">="),
37 ("or later", ">="),
38 ("and above", ">="),
39 ("or above", ">="),
40 ("and earlier", "<="),
41 ("or earlier", "<="),
42 ("and before", "<="),
43 ("or before", "<="),
44 ("and below", "<="),
45 ("or below", "<="),
46 ("and lower", "<="),
47 ("or lower", "<="),
48 ("and older", "<="),
49 ("or older", "<="),
50 ("and prior", "<="),
51 ];
52 for (phrase, operator) in bound_suffixes {
53 if let Some(versions) = strip_suffix_ignore_case(range, phrase) {
54 return bound_every_version(versions, operator, phrase);
55 }
56 }
57
58 if let Some(version) = range.strip_suffix('+')
59 && !version.contains([' ', ','])
60 {
61 return format!(">= {version}");
62 }
63
64 if let Some(requirement) = interval_to_requirement(range) {
65 return requirement;
66 }
67
68 let range = replace_ignore_case(range, " up to and including ", " - ");
69 let range = replace_ignore_case(&range, " through and including ", " - ");
70 let range = replace_ignore_case(&range, " through ", " - ");
71 let range = replace_ignore_case(&range, " until ", " - ");
72 let range = replace_ignore_case(&range, " to ", " - ");
73 replace_ignore_case(&range, " and ", ", ")
74}
75
76fn strip_prefix_ignore_case<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
82 let candidate = text.get(..prefix.len())?;
83 if candidate.eq_ignore_ascii_case(prefix) {
84 Some(text[prefix.len()..].trim_start())
85 } else {
86 None
87 }
88}
89
90fn strip_suffix_ignore_case<'a>(text: &'a str, suffix: &str) -> Option<&'a str> {
94 let start = text.len().checked_sub(suffix.len())?;
95 let candidate = text.get(start..)?;
96 if candidate.eq_ignore_ascii_case(suffix) {
97 Some(text[..start].trim_end())
98 } else {
99 None
100 }
101}
102
103pub fn replace_ignore_case(text: &str, from: &str, to: &str) -> String {
107 let lowercase_text = text.to_ascii_lowercase();
108 let lowercase_from = from.to_ascii_lowercase();
109 let mut result = String::new();
110 let mut rest_starts_at = 0;
111 for (index, _) in lowercase_text.match_indices(&lowercase_from) {
112 result.push_str(&text[rest_starts_at..index]);
113 result.push_str(to);
114 rest_starts_at = index + from.len();
115 }
116 result.push_str(&text[rest_starts_at..]);
117 result
118}
119
120pub fn replace_unusual_characters(range: &str) -> String {
134 range
135 .trim()
136 .trim_end_matches('.')
137 .replace('≤', "<=")
138 .replace('≥', ">=")
139 .replace("=<", "<=")
140 .replace("=>", ">=")
141 .replace("< =", "<=")
142 .replace("> =", ">=")
143 .replace("==", "=")
144 .replace("&&", ",")
145 .replace([';', ','], ",")
146 .replace(['–', '—'], " - ")
147 .replace(['\\', '`'], "")
148}