on bbcdcd0 help sign in
235 lines 8.0 kB history raw
1const SEPARATOR: char = '_';
3/// Increment an integer.
4///
5/// Supported bases:
6/// 2 with prefix 0b
7/// 8 with prefix 0o
8/// 10 with no prefix
9/// 16 with prefix 0x
10///
11/// An integer can contain `_` as a separator but may not start or end with a separator.
12/// Base 10 integers can go negative, but bases 2, 8, and 16 cannot.
13/// All addition and subtraction is saturating.
14pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
15 if selected_text.is_empty()
16 || selected_text.ends_with(SEPARATOR)
17 || selected_text.starts_with(SEPARATOR)
18 {
19 return None;
20 }
22 let radix = if selected_text.starts_with("0x") {
23 16
24 } else if selected_text.starts_with("0o") {
25 8
26 } else if selected_text.starts_with("0b") {
27 2
28 } else {
29 10
30 };
32 // Get separator indexes from right to left.
33 let separator_rtl_indexes: Vec<usize> = selected_text
34 .chars()
35 .rev()
36 .enumerate()
37 .filter_map(|(i, c)| if c == SEPARATOR { Some(i) } else { None })
38 .collect();
40 let word: String = selected_text.chars().filter(|&c| c != SEPARATOR).collect();
42 let mut new_text = if radix == 10 {
43 let number = &word;
44 let value = i128::from_str_radix(number, radix).ok()?;
45 let new_value = value.saturating_add(amount as i128);
47 let format_length = match (value.is_negative(), new_value.is_negative()) {
48 (true, false) => number.len() - 1,
49 (false, true) => number.len() + 1,
50 _ => number.len(),
51 } - separator_rtl_indexes.len();
53 if number.starts_with('0') || number.starts_with("-0") {
54 format!("{:01$}", new_value, format_length)
55 } else {
56 format!("{}", new_value)
57 }
58 } else {
59 let number = &word[2..];
60 let value = u128::from_str_radix(number, radix).ok()?;
61 let new_value = (value as i128).saturating_add(amount as i128);
62 let new_value = if new_value < 0 { 0 } else { new_value };
63 let format_length = selected_text.len() - 2 - separator_rtl_indexes.len();
65 match radix {
66 2 => format!("0b{:01$b}", new_value, format_length),
67 8 => format!("0o{:01$o}", new_value, format_length),
68 16 => {
69 let (lower_count, upper_count): (usize, usize) =
70 number.chars().fold((0, 0), |(lower, upper), c| {
71 (
72 lower + c.is_ascii_lowercase() as usize,
73 upper + c.is_ascii_uppercase() as usize,
74 )
75 });
76 if upper_count > lower_count {
77 format!("0x{:01$X}", new_value, format_length)
78 } else {
79 format!("0x{:01$x}", new_value, format_length)
80 }
81 }
82 _ => unimplemented!("radix not supported: {}", radix),
83 }
84 };
86 // Add separators from original number.
87 for &rtl_index in &separator_rtl_indexes {
88 if rtl_index < new_text.len() {
89 let new_index = new_text.len().saturating_sub(rtl_index);
90 if new_index > 0 {
91 new_text.insert(new_index, SEPARATOR);
92 }
93 }
94 }
96 // Add in additional separators if necessary.
97 if new_text.len() > selected_text.len() && !separator_rtl_indexes.is_empty() {
98 let spacing = match separator_rtl_indexes.as_slice() {
99 [.., b, a] => a - b - 1,
100 _ => separator_rtl_indexes[0],
101 };
103 let prefix_length = if radix == 10 { 0 } else { 2 };
104 if let Some(mut index) = new_text.find(SEPARATOR) {
105 while index - prefix_length > spacing {
106 index -= spacing;
107 new_text.insert(index, SEPARATOR);
108 }
109 }
110 }
112 Some(new_text)
115#[cfg(test)]
116mod test {
117 use super::*;
119 #[test]
120 fn test_increment_basic_decimal_numbers() {
121 let tests = [
122 ("100", 1, "101"),
123 ("100", -1, "99"),
124 ("99", 1, "100"),
125 ("100", 1000, "1100"),
126 ("100", -1000, "-900"),
127 ("-1", 1, "0"),
128 ("-1", 2, "1"),
129 ("1", -1, "0"),
130 ("1", -2, "-1"),
131 ];
133 for (original, amount, expected) in tests {
134 assert_eq!(increment(original, amount).unwrap(), expected);
135 }
136 }
138 #[test]
139 fn test_increment_basic_hexadecimal_numbers() {
140 let tests = [
141 ("0x0100", 1, "0x0101"),
142 ("0x0100", -1, "0x00ff"),
143 ("0x0001", -1, "0x0000"),
144 ("0x0000", -1, "0x0000"),
145 ("0xffffffffffffffff", 1, "0x10000000000000000"),
146 ("0xffffffffffffffff", 2, "0x10000000000000001"),
147 ("0xffffffffffffffff", -1, "0xfffffffffffffffe"),
148 ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"),
149 ("0xabcdef1234567890", 1, "0xabcdef1234567891"),
150 ];
152 for (original, amount, expected) in tests {
153 assert_eq!(increment(original, amount).unwrap(), expected);
154 }
155 }
157 #[test]
158 fn test_increment_basic_octal_numbers() {
159 let tests = [
160 ("0o0107", 1, "0o0110"),
161 ("0o0110", -1, "0o0107"),
162 ("0o0001", -1, "0o0000"),
163 ("0o7777", 1, "0o10000"),
164 ("0o1000", -1, "0o0777"),
165 ("0o0107", 10, "0o0121"),
166 ("0o0000", -1, "0o0000"),
167 ("0o1777777777777777777777", 1, "0o2000000000000000000000"),
168 ("0o1777777777777777777777", 2, "0o2000000000000000000001"),
169 ("0o1777777777777777777777", -1, "0o1777777777777777777776"),
170 ];
172 for (original, amount, expected) in tests {
173 assert_eq!(increment(original, amount).unwrap(), expected);
174 }
175 }
177 #[test]
178 fn test_increment_basic_binary_numbers() {
179 let tests = [
180 ("0b00000100", 1, "0b00000101"),
181 ("0b00000100", -1, "0b00000011"),
182 ("0b00000100", 2, "0b00000110"),
183 ("0b00000100", -2, "0b00000010"),
184 ("0b00000001", -1, "0b00000000"),
185 ("0b00111111", 10, "0b01001001"),
186 ("0b11111111", 1, "0b100000000"),
187 ("0b10000000", -1, "0b01111111"),
188 ("0b0000", -1, "0b0000"),
189 (
190 "0b1111111111111111111111111111111111111111111111111111111111111111",
191 1,
192 "0b10000000000000000000000000000000000000000000000000000000000000000",
193 ),
194 (
195 "0b1111111111111111111111111111111111111111111111111111111111111111",
196 2,
197 "0b10000000000000000000000000000000000000000000000000000000000000001",
198 ),
199 (
200 "0b1111111111111111111111111111111111111111111111111111111111111111",
201 -1,
202 "0b1111111111111111111111111111111111111111111111111111111111111110",
203 ),
204 ];
206 for (original, amount, expected) in tests {
207 assert_eq!(increment(original, amount).unwrap(), expected);
208 }
209 }
211 #[test]
212 fn test_increment_with_separators() {
213 let tests = [
214 ("999_999", 1, "1_000_000"),
215 ("1_000_000", -1, "999_999"),
216 ("-999_999", -1, "-1_000_000"),
217 ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
218 ("0x0000_0000", -1, "0x0000_0000"),
219 ("0x0000_0000_0000", -1, "0x0000_0000_0000"),
220 ("0b01111111_11111111", 1, "0b10000000_00000000"),
221 ("0b11111111_11111111", 1, "0b1_00000000_00000000"),
222 ];
224 for (original, amount, expected) in tests {
225 assert_eq!(increment(original, amount).unwrap(), expected);
226 }
227 }
229 #[test]
230 fn test_leading_and_trailing_separators_arent_a_match() {
231 assert_eq!(increment("9_", 1), None);
232 assert_eq!(increment("_9", 1), None);
233 assert_eq!(increment("_9_", 1), None);
234 }